From 1aa3ba390a92e93f0a21f30e72de3edc6dbc7a33 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Wed, 1 Apr 2015 23:17:22 -0700 Subject: [PATCH] Factor the guts of AsyncScroll into a base helper class. (bug 1139220 part 2, r=kgilbert) --- layout/generic/AsyncScrollBase.cpp | 135 ++++++++++++++++ layout/generic/AsyncScrollBase.h | 88 ++++++++++ layout/generic/moz.build | 2 + layout/generic/nsGfxScrollFrame.cpp | 243 +++++++--------------------- 4 files changed, 281 insertions(+), 187 deletions(-) create mode 100644 layout/generic/AsyncScrollBase.cpp create mode 100644 layout/generic/AsyncScrollBase.h diff --git a/layout/generic/AsyncScrollBase.cpp b/layout/generic/AsyncScrollBase.cpp new file mode 100644 index 00000000000..da1512308c4 --- /dev/null +++ b/layout/generic/AsyncScrollBase.cpp @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AsyncScrollBase.h" + +using namespace mozilla; + +AsyncScrollBase::AsyncScrollBase(nsPoint aStartPos) + : mIsFirstIteration(true) + , mStartPos(aStartPos) +{ +} + +void +AsyncScrollBase::Update(TimeStamp aTime, + nsPoint aDestination, + const nsSize& aCurrentVelocity) +{ + TimeDuration duration = ComputeDuration(aTime); + 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 + // instead continue with the existing timing function. + if (aDestination == mDestination && + aTime + duration > mStartTime + mDuration) + { + return; + } + + currentVelocity = VelocityAt(aTime); + mStartPos = PositionAt(aTime); + } + + mStartTime = aTime; + mDuration = duration; + mDestination = aDestination; + InitTimingFunction(mTimingFunctionX, mStartPos.x, currentVelocity.width, + aDestination.x); + InitTimingFunction(mTimingFunctionY, mStartPos.y, currentVelocity.height, + aDestination.y); + mIsFirstIteration = false; +} + +TimeDuration +AsyncScrollBase::ComputeDuration(TimeStamp aTime) +{ + // Average last 3 delta durations (rounding errors up to 2ms are negligible for us) + int32_t eventsDeltaMs = (aTime - mPrevEventTime[2]).ToMilliseconds() / 3; + mPrevEventTime[2] = mPrevEventTime[1]; + mPrevEventTime[1] = mPrevEventTime[0]; + mPrevEventTime[0] = aTime; + + // Modulate duration according to events rate (quicker events -> shorter durations). + // The desired effect is to use longer duration when scrolling slowly, such that + // it's easier to follow, but reduce the duration to make it feel more snappy when + // scrolling quickly. To reduce fluctuations of the duration, we average event + // intervals using the recent 4 timestamps (now + three prev -> 3 intervals). + int32_t durationMS = clamped(eventsDeltaMs * mIntervalRatio, mOriginMinMS, mOriginMaxMS); + + return TimeDuration::FromMilliseconds(durationMS); +} + +void +AsyncScrollBase::InitializeHistory(TimeStamp aTime) +{ + // Starting a new scroll (i.e. not when extending an existing scroll animation), + // create imaginary prev timestamps with maximum relevant intervals between them. + + // Longest relevant interval (which results in maximum duration) + TimeDuration maxDelta = TimeDuration::FromMilliseconds(mOriginMaxMS / mIntervalRatio); + mPrevEventTime[0] = aTime - maxDelta; + mPrevEventTime[1] = mPrevEventTime[0] - maxDelta; + mPrevEventTime[2] = mPrevEventTime[1] - maxDelta; +} + +const double kCurrentVelocityWeighting = 0.25; +const double kStopDecelerationWeighting = 0.4; + +void +AsyncScrollBase::InitTimingFunction(nsSMILKeySpline& aTimingFunction, + nscoord aCurrentPos, + nscoord aCurrentVelocity, + nscoord aDestination) +{ + if (aDestination == aCurrentPos || kCurrentVelocityWeighting == 0) { + aTimingFunction.Init(0, 0, 1 - kStopDecelerationWeighting, 1); + return; + } + + const TimeDuration oneSecond = TimeDuration::FromSeconds(1); + double slope = aCurrentVelocity * (mDuration / oneSecond) / (aDestination - aCurrentPos); + double normalization = sqrt(1.0 + slope * slope); + double dt = 1.0 / normalization * kCurrentVelocityWeighting; + double dxy = slope / normalization * kCurrentVelocityWeighting; + aTimingFunction.Init(dt, dxy, 1 - kStopDecelerationWeighting, 1); +} + +nsPoint +AsyncScrollBase::PositionAt(TimeStamp aTime) const +{ + double progressX = mTimingFunctionX.GetSplineValue(ProgressAt(aTime)); + double progressY = mTimingFunctionY.GetSplineValue(ProgressAt(aTime)); + return nsPoint(NSToCoordRound((1 - progressX) * mStartPos.x + progressX * mDestination.x), + NSToCoordRound((1 - progressY) * mStartPos.y + progressY * mDestination.y)); +} + +nsSize +AsyncScrollBase::VelocityAt(TimeStamp aTime) const +{ + double timeProgress = ProgressAt(aTime); + return nsSize(VelocityComponent(timeProgress, mTimingFunctionX, + mStartPos.x, mDestination.x), + VelocityComponent(timeProgress, mTimingFunctionY, + mStartPos.y, mDestination.y)); +} + +nscoord +AsyncScrollBase::VelocityComponent(double aTimeProgress, + const nsSMILKeySpline& aTimingFunction, + nscoord aStart, + nscoord aDestination) const +{ + double dt, dxy; + aTimingFunction.GetSplineDerivativeValues(aTimeProgress, dt, dxy); + if (dt == 0) + return dxy >= 0 ? nscoord_MAX : nscoord_MIN; + + const TimeDuration oneSecond = TimeDuration::FromSeconds(1); + double slope = dxy / dt; + return NSToCoordRound(slope * (aDestination - aStart) / (mDuration / oneSecond)); +} diff --git a/layout/generic/AsyncScrollBase.h b/layout/generic/AsyncScrollBase.h new file mode 100644 index 00000000000..5276a3ca8bd --- /dev/null +++ b/layout/generic/AsyncScrollBase.h @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_layout_AsyncScrollBase_h_ +#define mozilla_layout_AsyncScrollBase_h_ + +#include "mozilla/TimeStamp.h" +#include "nsPoint.h" +#include "nsSMILKeySpline.h" + +namespace mozilla { + +// This is the base class for driving scroll wheel animation on both the +// compositor and main thread. +class AsyncScrollBase +{ +public: + typedef mozilla::TimeStamp TimeStamp; + typedef mozilla::TimeDuration TimeDuration; + + AsyncScrollBase(nsPoint aStartPos); + + void Update(TimeStamp aTime, + nsPoint aDestination, + const nsSize& aCurrentVelocity); + + // Get the velocity at a point in time in nscoords/sec. + nsSize VelocityAt(TimeStamp aTime) const; + + // Returns the expected scroll position at a given point in time, in app + // units, relative to the scroll frame. + nsPoint PositionAt(TimeStamp aTime) const; + +protected: + double ProgressAt(TimeStamp aTime) const { + return clamped((aTime - mStartTime) / mDuration, 0.0, 1.0); + } + + nscoord VelocityComponent(double aTimeProgress, + const nsSMILKeySpline& aTimingFunction, + nscoord aStart, nscoord aDestination) const; + + // Calculate duration, possibly dynamically according to events rate and + // event origin. (also maintain previous timestamps - which are only used + // here). + TimeDuration ComputeDuration(TimeStamp aTime); + + // Initialize event history. + void InitializeHistory(TimeStamp aTime); + + // Initializes the timing function in such a way that the current velocity is + // preserved. + void InitTimingFunction(nsSMILKeySpline& aTimingFunction, + nscoord aCurrentPos, nscoord aCurrentVelocity, + nscoord aDestination); + + // mPrevEventTime holds previous 3 timestamps for intervals averaging (to + // reduce duration fluctuations). When AsyncScroll is constructed and no + // previous timestamps are available (indicated with mIsFirstIteration), + // initialize mPrevEventTime using imaginary previous timestamps with maximum + // relevant intervals between them. + TimeStamp mPrevEventTime[3]; + bool mIsFirstIteration; + + TimeStamp mStartTime; + + // Cached Preferences value. + // + // These values are minimum and maximum animation duration per event origin, + // and a global ratio which defines how longer is the animation's duration + // compared to the average recent events intervals (such that for a relatively + // consistent events rate, the next event arrives before current animation ends) + int32_t mOriginMinMS; + int32_t mOriginMaxMS; + double mIntervalRatio; + + nsPoint mStartPos; + TimeDuration mDuration; + nsPoint mDestination; + nsSMILKeySpline mTimingFunctionX; + nsSMILKeySpline mTimingFunctionY; +}; + +} + +#endif // mozilla_layout_AsyncScrollBase_h_ diff --git a/layout/generic/moz.build b/layout/generic/moz.build index 993db02cac4..c8639b637eb 100644 --- a/layout/generic/moz.build +++ b/layout/generic/moz.build @@ -61,6 +61,7 @@ with Files('nsVideoFrame.*'): BUG_COMPONENT = ('Core', 'Video/Audio') EXPORTS += [ + 'AsyncScrollBase.h', 'nsCanvasFrame.h', 'nsContainerFrame.h', 'nsDirection.h', @@ -104,6 +105,7 @@ EXPORTS.mozilla.layout += [ ] UNIFIED_SOURCES += [ + 'AsyncScrollBase.cpp', 'FrameChildList.cpp', 'MathMLTextRunFactory.cpp', 'nsAbsoluteContainingBlock.cpp', diff --git a/layout/generic/nsGfxScrollFrame.cpp b/layout/generic/nsGfxScrollFrame.cpp index 2a66c8e56b1..9716df241b7 100644 --- a/layout/generic/nsGfxScrollFrame.cpp +++ b/layout/generic/nsGfxScrollFrame.cpp @@ -55,6 +55,7 @@ #include "nsIFrameInlines.h" #include "gfxPlatform.h" #include "gfxPrefs.h" +#include "AsyncScrollBase.h" #include #include #include @@ -1447,9 +1448,6 @@ NS_QUERYFRAME_TAIL_INHERITING(nsBoxFrame) #define SMOOTH_SCROLL_PREF_NAME "general.smoothScroll" -const double kCurrentVelocityWeighting = 0.25; -const double kStopDecelerationWeighting = 0.4; - // AsyncSmoothMSDScroll has ref counting. class ScrollFrameHelper::AsyncSmoothMSDScroll final : public nsARefreshObserver { public: @@ -1581,14 +1579,16 @@ private: }; // AsyncScroll has ref counting. -class ScrollFrameHelper::AsyncScroll final : public nsARefreshObserver { +class ScrollFrameHelper::AsyncScroll final + : public nsARefreshObserver, + public AsyncScrollBase +{ public: typedef mozilla::TimeStamp TimeStamp; typedef mozilla::TimeDuration TimeDuration; explicit AsyncScroll(nsPoint aStartPos) - : mIsFirstIteration(true) - , mStartPos(aStartPos) + : AsyncScrollBase(aStartPos) , mCallee(nullptr) {} @@ -1599,9 +1599,6 @@ private: } public: - nsPoint PositionAt(TimeStamp aTime); - nsSize VelocityAt(TimeStamp aTime); // In nscoords per second - void InitSmoothScroll(TimeStamp aTime, nsPoint aDestination, nsIAtom *aOrigin, const nsRect& aRange, const nsSize& aCurrentVelocity); @@ -1613,53 +1610,15 @@ public: return aTime > mStartTime + mDuration; // XXX or if we've hit the wall } - TimeStamp mStartTime; - - // mPrevEventTime holds previous 3 timestamps for intervals averaging (to - // reduce duration fluctuations). When AsyncScroll is constructed and no - // previous timestamps are available (indicated with mIsFirstIteration), - // initialize mPrevEventTime using imaginary previous timestamps with maximum - // relevant intervals between them. - TimeStamp mPrevEventTime[3]; - bool mIsFirstIteration; - - // Cached Preferences values to avoid re-reading them when extending an existing - // animation for the same event origin (can be as frequent as every 10(!)ms for - // a quick roll of the mouse wheel). - // These values are minimum and maximum animation duration per event origin, - // and a global ratio which defines how longer is the animation's duration - // compared to the average recent events intervals (such that for a relatively - // consistent events rate, the next event arrives before current animation ends) + // Most recent scroll origin. nsCOMPtr mOrigin; - int32_t mOriginMinMS; - int32_t mOriginMaxMS; - double mIntervalRatio; - TimeDuration mDuration; - nsPoint mStartPos; - nsPoint mDestination; // Allowed destination positions around mDestination nsRect mRange; - nsSMILKeySpline mTimingFunctionX; - nsSMILKeySpline mTimingFunctionY; bool mIsSmoothScroll; -protected: - double ProgressAt(TimeStamp aTime) { - return clamped((aTime - mStartTime) / mDuration, 0.0, 1.0); - } - - nscoord VelocityComponent(double aTimeProgress, - nsSMILKeySpline& aTimingFunction, - nscoord aStart, nscoord aDestination); - - // Initializes the timing function in such a way that the current velocity is - // preserved. - void InitTimingFunction(nsSMILKeySpline& aTimingFunction, - nscoord aCurrentPos, nscoord aCurrentVelocity, - nscoord aDestination); - - TimeDuration CalcDurationForEventTime(TimeStamp aTime, nsIAtom *aOrigin); +private: + void InitPreferences(TimeStamp aTime, nsIAtom *aOrigin); // The next section is observer/callback management // Bodies of WillRefresh and RefreshDriver contain ScrollFrameHelper specific code. @@ -1707,162 +1666,72 @@ private: } }; -nsPoint -ScrollFrameHelper::AsyncScroll::PositionAt(TimeStamp aTime) { - double progressX = mTimingFunctionX.GetSplineValue(ProgressAt(aTime)); - double progressY = mTimingFunctionY.GetSplineValue(ProgressAt(aTime)); - return nsPoint(NSToCoordRound((1 - progressX) * mStartPos.x + progressX * mDestination.x), - NSToCoordRound((1 - progressY) * mStartPos.y + progressY * mDestination.y)); -} - -nsSize -ScrollFrameHelper::AsyncScroll::VelocityAt(TimeStamp aTime) { - double timeProgress = ProgressAt(aTime); - return nsSize(VelocityComponent(timeProgress, mTimingFunctionX, - mStartPos.x, mDestination.x), - VelocityComponent(timeProgress, mTimingFunctionY, - mStartPos.y, mDestination.y)); -} - /* * Calculate duration, possibly dynamically according to events rate and event origin. * (also maintain previous timestamps - which are only used here). */ -TimeDuration -ScrollFrameHelper:: -AsyncScroll::CalcDurationForEventTime(TimeStamp aTime, nsIAtom *aOrigin) { +void +ScrollFrameHelper::AsyncScroll::InitPreferences(TimeStamp aTime, nsIAtom *aOrigin) +{ if (!aOrigin){ aOrigin = nsGkAtoms::other; } // Read preferences only on first iteration or for a different event origin. - if (mIsFirstIteration || aOrigin != mOrigin) { - mOrigin = aOrigin; - mOriginMinMS = mOriginMaxMS = 0; - bool isOriginSmoothnessEnabled = false; - mIntervalRatio = 1; - - // Default values for all preferences are defined in all.js - static const int32_t kDefaultMinMS = 150, kDefaultMaxMS = 150; - static const bool kDefaultIsSmoothEnabled = true; - - nsAutoCString originName; - aOrigin->ToUTF8String(originName); - nsAutoCString prefBase = NS_LITERAL_CSTRING("general.smoothScroll.") + originName; - - isOriginSmoothnessEnabled = Preferences::GetBool(prefBase.get(), kDefaultIsSmoothEnabled); - if (isOriginSmoothnessEnabled) { - nsAutoCString prefMin = prefBase + NS_LITERAL_CSTRING(".durationMinMS"); - nsAutoCString prefMax = prefBase + NS_LITERAL_CSTRING(".durationMaxMS"); - mOriginMinMS = Preferences::GetInt(prefMin.get(), kDefaultMinMS); - mOriginMaxMS = Preferences::GetInt(prefMax.get(), kDefaultMaxMS); - - static const int32_t kSmoothScrollMaxAllowedAnimationDurationMS = 10000; - mOriginMaxMS = clamped(mOriginMaxMS, 0, kSmoothScrollMaxAllowedAnimationDurationMS); - mOriginMinMS = clamped(mOriginMinMS, 0, mOriginMaxMS); - } - - // Keep the animation duration longer than the average event intervals - // (to "connect" consecutive scroll animations before the scroll comes to a stop). - static const double kDefaultDurationToIntervalRatio = 2; // Duplicated at all.js - mIntervalRatio = Preferences::GetInt("general.smoothScroll.durationToIntervalRatio", - kDefaultDurationToIntervalRatio * 100) / 100.0; - - // Duration should be at least as long as the intervals -> ratio is at least 1 - mIntervalRatio = std::max(1.0, mIntervalRatio); - - if (mIsFirstIteration) { - // Starting a new scroll (i.e. not when extending an existing scroll animation), - // create imaginary prev timestamps with maximum relevant intervals between them. - - // Longest relevant interval (which results in maximum duration) - TimeDuration maxDelta = TimeDuration::FromMilliseconds(mOriginMaxMS / mIntervalRatio); - mPrevEventTime[0] = aTime - maxDelta; - mPrevEventTime[1] = mPrevEventTime[0] - maxDelta; - mPrevEventTime[2] = mPrevEventTime[1] - maxDelta; - } + if (!mIsFirstIteration && aOrigin == mOrigin) { + return; } - // Average last 3 delta durations (rounding errors up to 2ms are negligible for us) - int32_t eventsDeltaMs = (aTime - mPrevEventTime[2]).ToMilliseconds() / 3; - mPrevEventTime[2] = mPrevEventTime[1]; - mPrevEventTime[1] = mPrevEventTime[0]; - mPrevEventTime[0] = aTime; + mOrigin = aOrigin; + mOriginMinMS = mOriginMaxMS = 0; + bool isOriginSmoothnessEnabled = false; + mIntervalRatio = 1; - // Modulate duration according to events rate (quicker events -> shorter durations). - // The desired effect is to use longer duration when scrolling slowly, such that - // it's easier to follow, but reduce the duration to make it feel more snappy when - // scrolling quickly. To reduce fluctuations of the duration, we average event - // intervals using the recent 4 timestamps (now + three prev -> 3 intervals). - int32_t durationMS = clamped(eventsDeltaMs * mIntervalRatio, mOriginMinMS, mOriginMaxMS); + // Default values for all preferences are defined in all.js + static const int32_t kDefaultMinMS = 150, kDefaultMaxMS = 150; + static const bool kDefaultIsSmoothEnabled = true; - return TimeDuration::FromMilliseconds(durationMS); + nsAutoCString originName; + aOrigin->ToUTF8String(originName); + nsAutoCString prefBase = NS_LITERAL_CSTRING("general.smoothScroll.") + originName; + + isOriginSmoothnessEnabled = Preferences::GetBool(prefBase.get(), kDefaultIsSmoothEnabled); + if (isOriginSmoothnessEnabled) { + nsAutoCString prefMin = prefBase + NS_LITERAL_CSTRING(".durationMinMS"); + nsAutoCString prefMax = prefBase + NS_LITERAL_CSTRING(".durationMaxMS"); + mOriginMinMS = Preferences::GetInt(prefMin.get(), kDefaultMinMS); + mOriginMaxMS = Preferences::GetInt(prefMax.get(), kDefaultMaxMS); + + static const int32_t kSmoothScrollMaxAllowedAnimationDurationMS = 10000; + mOriginMaxMS = clamped(mOriginMaxMS, 0, kSmoothScrollMaxAllowedAnimationDurationMS); + mOriginMinMS = clamped(mOriginMinMS, 0, mOriginMaxMS); + } + + // Keep the animation duration longer than the average event intervals + // (to "connect" consecutive scroll animations before the scroll comes to a stop). + static const double kDefaultDurationToIntervalRatio = 2; // Duplicated at all.js + mIntervalRatio = Preferences::GetInt("general.smoothScroll.durationToIntervalRatio", + kDefaultDurationToIntervalRatio * 100) / 100.0; + + // Duration should be at least as long as the intervals -> ratio is at least 1 + mIntervalRatio = std::max(1.0, mIntervalRatio); + + if (mIsFirstIteration) { + InitializeHistory(aTime); + } } void ScrollFrameHelper::AsyncScroll::InitSmoothScroll(TimeStamp aTime, - nsPoint aDestination, - nsIAtom *aOrigin, - const nsRect& aRange, - const nsSize& aCurrentVelocity) { + nsPoint aDestination, + nsIAtom *aOrigin, + const nsRect& aRange, + const nsSize& aCurrentVelocity) +{ + InitPreferences(aTime, aOrigin); mRange = aRange; - TimeDuration duration = CalcDurationForEventTime(aTime, aOrigin); - 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 - // instead continue with the existing timing function. - if (aDestination == mDestination && - aTime + duration > mStartTime + mDuration) - return; - currentVelocity = VelocityAt(aTime); - mStartPos = PositionAt(aTime); - } - mStartTime = aTime; - mDuration = duration; - mDestination = aDestination; - InitTimingFunction(mTimingFunctionX, mStartPos.x, currentVelocity.width, - aDestination.x); - InitTimingFunction(mTimingFunctionY, mStartPos.y, currentVelocity.height, - aDestination.y); - mIsFirstIteration = false; -} - - -nscoord -ScrollFrameHelper::AsyncScroll::VelocityComponent(double aTimeProgress, - nsSMILKeySpline& aTimingFunction, - nscoord aStart, - nscoord aDestination) -{ - double dt, dxy; - aTimingFunction.GetSplineDerivativeValues(aTimeProgress, dt, dxy); - if (dt == 0) - return dxy >= 0 ? nscoord_MAX : nscoord_MIN; - - const TimeDuration oneSecond = TimeDuration::FromSeconds(1); - double slope = dxy / dt; - return NSToCoordRound(slope * (aDestination - aStart) / (mDuration / oneSecond)); -} - -void -ScrollFrameHelper::AsyncScroll::InitTimingFunction(nsSMILKeySpline& aTimingFunction, - nscoord aCurrentPos, - nscoord aCurrentVelocity, - nscoord aDestination) -{ - if (aDestination == aCurrentPos || kCurrentVelocityWeighting == 0) { - aTimingFunction.Init(0, 0, 1 - kStopDecelerationWeighting, 1); - return; - } - - const TimeDuration oneSecond = TimeDuration::FromSeconds(1); - double slope = aCurrentVelocity * (mDuration / oneSecond) / (aDestination - aCurrentPos); - double normalization = sqrt(1.0 + slope * slope); - double dt = 1.0 / normalization * kCurrentVelocityWeighting; - double dxy = slope / normalization * kCurrentVelocityWeighting; - aTimingFunction.Init(dt, dxy, 1 - kStopDecelerationWeighting, 1); + Update(aTime, aDestination, aCurrentVelocity); } bool