blob: 059bc9d51270ef2ea31dcb07ad392d45768ea143 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/renderer/input/input_scroll_elasticity_controller.h"
#include <math.h>
// ScrollElasticityController and ScrollElasticityControllerClient are based on
// WebKit/Source/platform/mac/ScrollElasticityController.mm
/*
* Copyright (C) 2011 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
namespace content {
namespace {
// TODO(ccameron): This is advertised as system uptime, but it used to compute
// deltas against the event timestamps, which are in seconds since epoch. Find
// out which is right. Also, avoid querying time, and use frame time instead.
double SystemUptime() {
return base::Time::Now().ToDoubleT();
}
const float kScrollVelocityZeroingTimeout = 0.10f;
const float kRubberbandDirectionLockStretchRatio = 1;
const float kRubberbandMinimumRequiredDeltaBeforeStretch = 10;
const float kRubberbandStiffness = 20;
const float kRubberbandAmplitude = 0.31f;
const float kRubberbandPeriod = 1.6f;
float ElasticDeltaForTimeDelta(float initial_position,
float initial_velocity,
float elapsed_time) {
float amplitude = kRubberbandAmplitude;
float period = kRubberbandPeriod;
float critical_dampening_factor =
expf((-elapsed_time * kRubberbandStiffness) / period);
return (initial_position + (-initial_velocity * elapsed_time * amplitude)) *
critical_dampening_factor;
}
float ElasticDeltaForReboundDelta(float delta) {
float stiffness = std::max(kRubberbandStiffness, 1.0f);
return delta / stiffness;
}
float ReboundDeltaForElasticDelta(float delta) {
return delta * kRubberbandStiffness;
}
float ScrollWheelMultiplier() {
static float multiplier = -1;
if (multiplier < 0) {
// TODO(ccameron): Find a place fo this in an Objective C file, or find an
// equivalent C call.
// multiplier = [[NSUserDefaults standardUserDefaults]
// floatForKey:@"NSScrollWheelMultiplier"];
if (multiplier <= 0)
multiplier = 1;
}
return multiplier;
}
} // namespace
ScrollElasticityController::ScrollElasticityController(
ScrollElasticityControllerClient* client)
: client_(client),
in_scroll_gesture_(false),
has_scrolled_(false),
momentum_scroll_in_progress_(false),
ignore_momentum_scrolls_(false),
last_momentum_scroll_timestamp_(0),
snap_rubberband_timer_is_active_(false) {
}
bool ScrollElasticityController::HandleWheelEvent(
const blink::WebMouseWheelEvent& wheel_event) {
if (wheel_event.phase == blink::WebMouseWheelEvent::PhaseMayBegin)
return false;
if (wheel_event.phase == blink::WebMouseWheelEvent::PhaseBegan) {
in_scroll_gesture_ = true;
has_scrolled_ = false;
momentum_scroll_in_progress_ = false;
ignore_momentum_scrolls_ = false;
last_momentum_scroll_timestamp_ = 0;
momentum_velocity_ = gfx::Vector2dF();
gfx::Vector2dF stretch_amount = client_->StretchAmount();
stretch_scroll_force_.set_x(
ReboundDeltaForElasticDelta(stretch_amount.x()));
stretch_scroll_force_.set_y(
ReboundDeltaForElasticDelta(stretch_amount.y()));
overflow_scroll_delta_ = gfx::Vector2dF();
StopSnapRubberbandTimer();
// TODO(erikchen): Use the commented out line once Chromium uses the
// return value correctly.
// crbug.com/375512
// return ShouldHandleEvent(wheel_event);
// This logic is incorrect, since diagonal wheel events are not consumed.
if (client_->PinnedInDirection(gfx::Vector2dF(-wheel_event.deltaX, 0))) {
if (wheel_event.deltaX > 0 && !wheel_event.canRubberbandLeft)
return false;
if (wheel_event.deltaX < 0 && !wheel_event.canRubberbandRight)
return false;
}
return true;
}
if (wheel_event.phase == blink::WebMouseWheelEvent::PhaseEnded ||
wheel_event.phase == blink::WebMouseWheelEvent::PhaseCancelled) {
SnapRubberband();
return has_scrolled_;
}
bool isMomentumScrollEvent =
(wheel_event.momentumPhase != blink::WebMouseWheelEvent::PhaseNone);
if (ignore_momentum_scrolls_ &&
(isMomentumScrollEvent || snap_rubberband_timer_is_active_)) {
if (wheel_event.momentumPhase == blink::WebMouseWheelEvent::PhaseEnded) {
ignore_momentum_scrolls_ = false;
return true;
}
return false;
}
if (!ShouldHandleEvent(wheel_event))
return false;
float delta_x = overflow_scroll_delta_.x() - wheel_event.deltaX;
float delta_y = overflow_scroll_delta_.y() - wheel_event.deltaY;
float event_coalesced_delta_x = -wheel_event.deltaX;
float event_coalesced_delta_y = -wheel_event.deltaY;
// Reset overflow values because we may decide to remove delta at various
// points and put it into overflow.
overflow_scroll_delta_ = gfx::Vector2dF();
gfx::Vector2dF stretch_amount = client_->StretchAmount();
bool is_vertically_stretched = stretch_amount.y() != 0.f;
bool is_horizontally_stretched = stretch_amount.x() != 0.f;
// Slightly prefer scrolling vertically by applying the = case to delta_y
if (fabsf(delta_y) >= fabsf(delta_x))
delta_x = 0;
else
delta_y = 0;
bool should_stretch = false;
blink::WebMouseWheelEvent::Phase momentum_phase = wheel_event.momentumPhase;
// If we are starting momentum scrolling then do some setup.
if (!momentum_scroll_in_progress_ &&
(momentum_phase == blink::WebMouseWheelEvent::PhaseBegan ||
momentum_phase == blink::WebMouseWheelEvent::PhaseChanged)) {
momentum_scroll_in_progress_ = true;
// Start the snap rubber band timer if it's not running. This is needed to
// snap back from the over scroll caused by momentum events.
if (!snap_rubberband_timer_is_active_ && start_time_ == base::Time())
SnapRubberband();
}
double time_delta =
wheel_event.timeStampSeconds - last_momentum_scroll_timestamp_;
if (in_scroll_gesture_ || momentum_scroll_in_progress_) {
if (last_momentum_scroll_timestamp_ && time_delta > 0 &&
time_delta < kScrollVelocityZeroingTimeout) {
momentum_velocity_.set_x(event_coalesced_delta_x / (float)time_delta);
momentum_velocity_.set_y(event_coalesced_delta_y / (float)time_delta);
last_momentum_scroll_timestamp_ = wheel_event.timeStampSeconds;
} else {
last_momentum_scroll_timestamp_ = wheel_event.timeStampSeconds;
momentum_velocity_ = gfx::Vector2dF();
}
if (is_vertically_stretched) {
if (!is_horizontally_stretched &&
client_->PinnedInDirection(gfx::Vector2dF(delta_x, 0))) {
// Stretching only in the vertical.
if (delta_y != 0 &&
(fabsf(delta_x / delta_y) < kRubberbandDirectionLockStretchRatio))
delta_x = 0;
else if (fabsf(delta_x) <
kRubberbandMinimumRequiredDeltaBeforeStretch) {
overflow_scroll_delta_.set_x(overflow_scroll_delta_.x() + delta_x);
delta_x = 0;
} else
overflow_scroll_delta_.set_x(overflow_scroll_delta_.x() + delta_x);
}
} else if (is_horizontally_stretched) {
// Stretching only in the horizontal.
if (client_->PinnedInDirection(gfx::Vector2dF(0, delta_y))) {
if (delta_x != 0 &&
(fabsf(delta_y / delta_x) < kRubberbandDirectionLockStretchRatio))
delta_y = 0;
else if (fabsf(delta_y) <
kRubberbandMinimumRequiredDeltaBeforeStretch) {
overflow_scroll_delta_.set_y(overflow_scroll_delta_.y() + delta_y);
delta_y = 0;
} else
overflow_scroll_delta_.set_y(overflow_scroll_delta_.y() + delta_y);
}
} else {
// Not stretching at all yet.
if (client_->PinnedInDirection(gfx::Vector2dF(delta_x, delta_y))) {
if (fabsf(delta_y) >= fabsf(delta_x)) {
if (fabsf(delta_x) < kRubberbandMinimumRequiredDeltaBeforeStretch) {
overflow_scroll_delta_.set_x(overflow_scroll_delta_.x() + delta_x);
delta_x = 0;
} else
overflow_scroll_delta_.set_x(overflow_scroll_delta_.x() + delta_x);
}
should_stretch = true;
}
}
}
if (delta_x != 0 || delta_y != 0) {
has_scrolled_ = true;
if (!(should_stretch || is_vertically_stretched ||
is_horizontally_stretched)) {
if (delta_y != 0) {
delta_y *= ScrollWheelMultiplier();
client_->ImmediateScrollBy(gfx::Vector2dF(0, delta_y));
}
if (delta_x != 0) {
delta_x *= ScrollWheelMultiplier();
client_->ImmediateScrollBy(gfx::Vector2dF(delta_x, 0));
}
} else {
if (!client_->AllowsHorizontalStretching()) {
delta_x = 0;
event_coalesced_delta_x = 0;
} else if ((delta_x != 0) && !is_horizontally_stretched &&
!client_->PinnedInDirection(gfx::Vector2dF(delta_x, 0))) {
delta_x *= ScrollWheelMultiplier();
client_->ImmediateScrollByWithoutContentEdgeConstraints(
gfx::Vector2dF(delta_x, 0));
delta_x = 0;
}
if (!client_->AllowsVerticalStretching()) {
delta_y = 0;
event_coalesced_delta_y = 0;
} else if ((delta_y != 0) && !is_vertically_stretched &&
!client_->PinnedInDirection(gfx::Vector2dF(0, delta_y))) {
delta_y *= ScrollWheelMultiplier();
client_->ImmediateScrollByWithoutContentEdgeConstraints(
gfx::Vector2dF(0, delta_y));
delta_y = 0;
}
gfx::Vector2dF stretch_amount = client_->StretchAmount();
if (momentum_scroll_in_progress_) {
if ((client_->PinnedInDirection(gfx::Vector2dF(
event_coalesced_delta_x, event_coalesced_delta_y)) ||
(fabsf(event_coalesced_delta_x) + fabsf(event_coalesced_delta_y) <=
0)) &&
last_momentum_scroll_timestamp_) {
ignore_momentum_scrolls_ = true;
momentum_scroll_in_progress_ = false;
SnapRubberband();
}
}
stretch_scroll_force_.set_x(stretch_scroll_force_.x() + delta_x);
stretch_scroll_force_.set_y(stretch_scroll_force_.y() + delta_y);
gfx::Vector2dF damped_delta(
ceilf(ElasticDeltaForReboundDelta(stretch_scroll_force_.x())),
ceilf(ElasticDeltaForReboundDelta(stretch_scroll_force_.y())));
client_->ImmediateScrollByWithoutContentEdgeConstraints(damped_delta -
stretch_amount);
}
}
if (momentum_scroll_in_progress_ &&
momentum_phase == blink::WebMouseWheelEvent::PhaseEnded) {
momentum_scroll_in_progress_ = false;
ignore_momentum_scrolls_ = false;
last_momentum_scroll_timestamp_ = 0;
}
return true;
}
namespace {
float RoundTowardZero(float num) {
return num > 0 ? ceilf(num - 0.5f) : floorf(num + 0.5f);
}
float RoundToDevicePixelTowardZero(float num) {
float rounded_num = roundf(num);
if (fabs(num - rounded_num) < 0.125)
num = rounded_num;
return RoundTowardZero(num);
}
} // namespace
void ScrollElasticityController::SnapRubberbandTimerFired() {
if (!momentum_scroll_in_progress_ || ignore_momentum_scrolls_) {
float time_delta = (base::Time::Now() - start_time_).InSecondsF();
if (start_stretch_ == gfx::Vector2dF()) {
start_stretch_ = client_->StretchAmount();
if (start_stretch_ == gfx::Vector2dF()) {
StopSnapRubberbandTimer();
stretch_scroll_force_ = gfx::Vector2dF();
start_time_ = base::Time();
orig_origin_ = gfx::Vector2dF();
orig_velocity_ = gfx::Vector2dF();
return;
}
orig_origin_ = client_->AbsoluteScrollPosition() - start_stretch_;
orig_velocity_ = momentum_velocity_;
// Just like normal scrolling, prefer vertical rubberbanding
if (fabsf(orig_velocity_.y()) >= fabsf(orig_velocity_.x()))
orig_velocity_.set_x(0);
// Don't rubber-band horizontally if it's not possible to scroll
// horizontally
if (!client_->CanScrollHorizontally())
orig_velocity_.set_x(0);
// Don't rubber-band vertically if it's not possible to scroll
// vertically
if (!client_->CanScrollVertically())
orig_velocity_.set_y(0);
}
gfx::Vector2dF delta(
RoundToDevicePixelTowardZero(ElasticDeltaForTimeDelta(
start_stretch_.x(), -orig_velocity_.x(), time_delta)),
RoundToDevicePixelTowardZero(ElasticDeltaForTimeDelta(
start_stretch_.y(), -orig_velocity_.y(), time_delta)));
if (fabs(delta.x()) >= 1 || fabs(delta.y()) >= 1) {
client_->ImmediateScrollByWithoutContentEdgeConstraints(
gfx::Vector2dF(delta.x(), delta.y()) - client_->StretchAmount());
gfx::Vector2dF new_stretch = client_->StretchAmount();
stretch_scroll_force_.set_x(ReboundDeltaForElasticDelta(new_stretch.x()));
stretch_scroll_force_.set_y(ReboundDeltaForElasticDelta(new_stretch.y()));
} else {
client_->AdjustScrollPositionToBoundsIfNecessary();
StopSnapRubberbandTimer();
stretch_scroll_force_ = gfx::Vector2dF();
start_time_ = base::Time();
start_stretch_ = gfx::Vector2dF();
orig_origin_ = gfx::Vector2dF();
orig_velocity_ = gfx::Vector2dF();
}
} else {
start_time_ = base::Time::Now();
start_stretch_ = gfx::Vector2dF();
}
}
bool ScrollElasticityController::IsRubberbandInProgress() const {
if (!in_scroll_gesture_ && !momentum_scroll_in_progress_ &&
!snap_rubberband_timer_is_active_)
return false;
return !client_->StretchAmount().IsZero();
}
void ScrollElasticityController::StopSnapRubberbandTimer() {
client_->StopSnapRubberbandTimer();
snap_rubberband_timer_is_active_ = false;
}
void ScrollElasticityController::SnapRubberband() {
double time_delta = SystemUptime() - last_momentum_scroll_timestamp_;
if (last_momentum_scroll_timestamp_ &&
time_delta >= kScrollVelocityZeroingTimeout)
momentum_velocity_ = gfx::Vector2dF();
in_scroll_gesture_ = false;
if (snap_rubberband_timer_is_active_)
return;
start_stretch_ = gfx::Vector2dF();
orig_origin_ = gfx::Vector2dF();
orig_velocity_ = gfx::Vector2dF();
// If there's no momentum scroll or stretch amount, no need to start the
// timer.
if (!momentum_scroll_in_progress_ &&
client_->StretchAmount() == gfx::Vector2dF()) {
start_time_ = base::Time();
stretch_scroll_force_ = gfx::Vector2dF();
return;
}
start_time_ = base::Time::Now();
client_->StartSnapRubberbandTimer();
snap_rubberband_timer_is_active_ = true;
}
bool ScrollElasticityController::ShouldHandleEvent(
const blink::WebMouseWheelEvent& wheel_event) {
// Once any scrolling has happened, all future events should be handled.
if (has_scrolled_)
return true;
// The event can't cause scrolling to start if its delta is 0.
if (wheel_event.deltaX == 0 && wheel_event.deltaY == 0)
return false;
// If the client isn't pinned, then the event is guaranteed to cause
// scrolling.
if (!client_->PinnedInDirection(gfx::Vector2dF(-wheel_event.deltaX, 0)))
return true;
// If the event is pinned, then the client can't scroll, but it might rubber
// band.
// Check if the event allows rubber banding.
if (wheel_event.deltaY == 0) {
if (wheel_event.deltaX > 0 && !wheel_event.canRubberbandLeft)
return false;
if (wheel_event.deltaX < 0 && !wheel_event.canRubberbandRight)
return false;
}
// The event is going to either cause scrolling or rubber banding.
return true;
}
} // namespace content