blob: 01ad6416811ef43860f5f8f1eb6bbba1a33a81c1 [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 "third_party/blink/renderer/platform/widget/input/elastic_overscroll_controller.h"
#include <math.h>
#include <algorithm>
#include "base/bind.h"
#include "build/build_config.h"
#include "cc/input/input_handler.h"
#include "third_party/blink/renderer/platform/widget/input/elastic_overscroll_controller_bezier.h"
#include "third_party/blink/renderer/platform/widget/input/elastic_overscroll_controller_exponential.h"
#include "ui/base/ui_base_features.h"
#include "ui/events/types/scroll_types.h"
#include "ui/gfx/geometry/vector2d_conversions.h"
/*
* 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 blink {
namespace {
constexpr double kScrollVelocityZeroingTimeout = 0.10f;
constexpr double kRubberbandMinimumRequiredDeltaBeforeStretch = 10;
} // namespace
ElasticOverscrollController::ElasticOverscrollController(
cc::ScrollElasticityHelper* helper)
: helper_(helper),
state_(kStateInactive),
received_overscroll_update_(false) {}
std::unique_ptr<ElasticOverscrollController>
ElasticOverscrollController::Create(cc::ScrollElasticityHelper* helper) {
#if defined(OS_WIN)
return base::FeatureList::IsEnabled(features::kElasticOverscrollWin)
? std::make_unique<ElasticOverscrollControllerBezier>(helper)
: nullptr;
#endif
return std::make_unique<ElasticOverscrollControllerExponential>(helper);
}
void ElasticOverscrollController::ObserveRealScrollBegin(bool enter_momentum,
bool leave_momentum) {
if (enter_momentum) {
if (state_ == kStateInactive)
state_ = kStateMomentumScroll;
} else if (leave_momentum) {
scroll_velocity_ = gfx::Vector2dF();
last_scroll_event_timestamp_ = base::TimeTicks();
state_ = kStateActiveScroll;
pending_overscroll_delta_ = gfx::Vector2dF();
}
}
void ElasticOverscrollController::ObserveScrollUpdate(
const gfx::Vector2dF& event_delta,
const gfx::Vector2dF& unused_scroll_delta,
const base::TimeTicks& event_timestamp,
const cc::OverscrollBehavior overscroll_behavior,
bool has_momentum) {
if (state_ == kStateMomentumAnimated || state_ == kStateInactive)
return;
if (!received_overscroll_update_ && !unused_scroll_delta.IsZero()) {
overscroll_behavior_ = overscroll_behavior;
received_overscroll_update_ = true;
}
UpdateVelocity(event_delta, event_timestamp);
Overscroll(unused_scroll_delta);
if (has_momentum && !helper_->StretchAmount().IsZero())
EnterStateMomentumAnimated(event_timestamp);
}
void ElasticOverscrollController::ObserveRealScrollEnd(
const base::TimeTicks event_timestamp) {
if (state_ == kStateMomentumAnimated || state_ == kStateInactive)
return;
if (helper_->StretchAmount().IsZero()) {
EnterStateInactive();
} else {
EnterStateMomentumAnimated(event_timestamp);
}
}
void ElasticOverscrollController::ObserveGestureEventAndResult(
const WebGestureEvent& gesture_event,
const cc::InputHandlerScrollResult& scroll_result) {
base::TimeTicks event_timestamp = gesture_event.TimeStamp();
switch (gesture_event.GetType()) {
case WebInputEvent::Type::kGestureScrollBegin: {
received_overscroll_update_ = false;
overscroll_behavior_ = cc::OverscrollBehavior();
if (gesture_event.data.scroll_begin.synthetic)
return;
bool enter_momentum = gesture_event.data.scroll_begin.inertial_phase ==
WebGestureEvent::InertialPhaseState::kMomentum;
bool leave_momentum =
gesture_event.data.scroll_begin.inertial_phase ==
WebGestureEvent::InertialPhaseState::kNonMomentum &&
gesture_event.data.scroll_begin.delta_hint_units ==
ui::ScrollGranularity::kScrollByPrecisePixel;
ObserveRealScrollBegin(enter_momentum, leave_momentum);
break;
}
case WebInputEvent::Type::kGestureScrollUpdate: {
gfx::Vector2dF event_delta(-gesture_event.data.scroll_update.delta_x,
-gesture_event.data.scroll_update.delta_y);
bool has_momentum = gesture_event.data.scroll_update.inertial_phase ==
WebGestureEvent::InertialPhaseState::kMomentum;
ObserveScrollUpdate(event_delta, scroll_result.unused_scroll_delta,
event_timestamp, scroll_result.overscroll_behavior,
has_momentum);
break;
}
case WebInputEvent::Type::kGestureScrollEnd: {
if (gesture_event.data.scroll_end.synthetic)
return;
ObserveRealScrollEnd(event_timestamp);
break;
}
default:
break;
}
}
void ElasticOverscrollController::UpdateVelocity(
const gfx::Vector2dF& event_delta,
const base::TimeTicks& event_timestamp) {
float time_delta =
(event_timestamp - last_scroll_event_timestamp_).InSecondsF();
if (time_delta < kScrollVelocityZeroingTimeout && time_delta > 0) {
scroll_velocity_ = gfx::Vector2dF(event_delta.x() / time_delta,
event_delta.y() / time_delta);
} else {
scroll_velocity_ = gfx::Vector2dF();
}
last_scroll_event_timestamp_ = event_timestamp;
}
void ElasticOverscrollController::Overscroll(
const gfx::Vector2dF& overscroll_delta) {
// The effect can be dynamically disabled by setting disallowing user
// scrolling. When disabled, disallow active or momentum overscrolling, but
// allow any current overscroll to animate back.
if (!helper_->IsUserScrollable())
return;
gfx::Vector2dF adjusted_overscroll_delta =
pending_overscroll_delta_ + overscroll_delta;
pending_overscroll_delta_ = gfx::Vector2dF();
// TODO (arakeri): Make this prefer the writing mode direction instead.
// Only allow one direction to overscroll at a time, and slightly prefer
// scrolling vertically by applying the equal case to delta_y.
if (fabsf(overscroll_delta.y()) >= fabsf(overscroll_delta.x()))
adjusted_overscroll_delta.set_x(0);
else
adjusted_overscroll_delta.set_y(0);
// Don't allow overscrolling in a direction where scrolling is possible.
if (!PinnedHorizontally(adjusted_overscroll_delta.x()))
adjusted_overscroll_delta.set_x(0);
if (!PinnedVertically(adjusted_overscroll_delta.y()))
adjusted_overscroll_delta.set_y(0);
// Don't allow overscrolling in a direction that has
// OverscrollBehaviorTypeNone.
if (overscroll_behavior_.x == cc::OverscrollBehavior::Type::kNone)
adjusted_overscroll_delta.set_x(0);
if (overscroll_behavior_.y == cc::OverscrollBehavior::Type::kNone)
adjusted_overscroll_delta.set_y(0);
// Require a minimum of 10 units of overscroll before starting the rubber-band
// stretch effect, so that small stray motions don't trigger it. If that
// minimum isn't met, save what remains in |pending_overscroll_delta_| for
// the next event.
gfx::Vector2dF old_stretch_amount = helper_->StretchAmount();
gfx::Vector2dF stretch_scroll_force_delta;
if (old_stretch_amount.x() != 0 ||
fabsf(adjusted_overscroll_delta.x()) >=
kRubberbandMinimumRequiredDeltaBeforeStretch) {
stretch_scroll_force_delta.set_x(adjusted_overscroll_delta.x());
} else {
pending_overscroll_delta_.set_x(adjusted_overscroll_delta.x());
}
if (old_stretch_amount.y() != 0 ||
fabsf(adjusted_overscroll_delta.y()) >=
kRubberbandMinimumRequiredDeltaBeforeStretch) {
stretch_scroll_force_delta.set_y(adjusted_overscroll_delta.y());
} else {
pending_overscroll_delta_.set_y(adjusted_overscroll_delta.y());
}
// Update the stretch amount according to the spring equations.
if (stretch_scroll_force_delta.IsZero())
return;
stretch_scroll_force_ += stretch_scroll_force_delta;
gfx::Vector2dF new_stretch_amount =
StretchAmountForAccumulatedOverscroll(stretch_scroll_force_);
helper_->SetStretchAmount(new_stretch_amount);
}
void ElasticOverscrollController::EnterStateInactive() {
DCHECK_NE(kStateInactive, state_);
DCHECK(helper_->StretchAmount().IsZero());
state_ = kStateInactive;
stretch_scroll_force_ = gfx::Vector2dF();
}
void ElasticOverscrollController::EnterStateMomentumAnimated(
const base::TimeTicks& triggering_event_timestamp) {
DCHECK_NE(kStateMomentumAnimated, state_);
state_ = kStateMomentumAnimated;
// If the scroller isn't stretched, there's nothing to animate.
if (helper_->StretchAmount().IsZero())
return;
momentum_animation_start_time_ = triggering_event_timestamp;
momentum_animation_initial_stretch_ = helper_->StretchAmount();
momentum_animation_initial_velocity_ = scroll_velocity_;
// Similarly to the logic in Overscroll, prefer vertical scrolling to
// horizontal scrolling.
if (fabsf(momentum_animation_initial_velocity_.y()) >=
fabsf(momentum_animation_initial_velocity_.x()))
momentum_animation_initial_velocity_.set_x(0);
if (!CanScrollHorizontally())
momentum_animation_initial_velocity_.set_x(0);
if (!CanScrollVertically())
momentum_animation_initial_velocity_.set_y(0);
DidEnterMomentumAnimatedState();
// TODO(crbug.com/394562): This can go away once input is batched to the front
// of the frame? Then Animate() would always happen after this, so it would
// have a chance to tick the animation there and would return if any
// animations were active.
helper_->RequestOneBeginFrame();
}
void ElasticOverscrollController::Animate(base::TimeTicks time) {
if (state_ != kStateMomentumAnimated)
return;
// If the new stretch amount is near zero, set it directly to zero and enter
// the inactive state.
const gfx::Vector2dF new_stretch_amount = StretchAmountForTimeDelta(
std::max(time - momentum_animation_start_time_, base::TimeDelta()));
if (fabs(new_stretch_amount.x()) < 1 && fabs(new_stretch_amount.y()) < 1) {
helper_->SetStretchAmount(gfx::Vector2dF());
EnterStateInactive();
return;
}
stretch_scroll_force_ =
AccumulatedOverscrollForStretchAmount(new_stretch_amount);
helper_->SetStretchAmount(new_stretch_amount);
// TODO(danakj): Make this a return value back to the compositor to have it
// schedule another frame and/or a draw. (Also, crbug.com/551138.)
helper_->RequestOneBeginFrame();
}
bool ElasticOverscrollController::PinnedHorizontally(float direction) const {
gfx::ScrollOffset scroll_offset = helper_->ScrollOffset();
gfx::ScrollOffset max_scroll_offset = helper_->MaxScrollOffset();
if (direction < 0)
return scroll_offset.x() <= 0;
if (direction > 0)
return scroll_offset.x() >= max_scroll_offset.x();
return false;
}
bool ElasticOverscrollController::PinnedVertically(float direction) const {
gfx::ScrollOffset scroll_offset = helper_->ScrollOffset();
gfx::ScrollOffset max_scroll_offset = helper_->MaxScrollOffset();
if (direction < 0)
return scroll_offset.y() <= 0;
if (direction > 0)
return scroll_offset.y() >= max_scroll_offset.y();
return false;
}
bool ElasticOverscrollController::CanScrollHorizontally() const {
return helper_->MaxScrollOffset().x() > 0;
}
bool ElasticOverscrollController::CanScrollVertically() const {
return helper_->MaxScrollOffset().y() > 0;
}
void ElasticOverscrollController::ReconcileStretchAndScroll() {
gfx::Vector2dF stretch = helper_->StretchAmount();
if (stretch.IsZero())
return;
gfx::ScrollOffset scroll_offset = helper_->ScrollOffset();
gfx::ScrollOffset max_scroll_offset = helper_->MaxScrollOffset();
// Compute stretch_adjustment which will be added to |stretch| and subtracted
// from the |scroll_offset|.
gfx::Vector2dF stretch_adjustment;
if (stretch.x() < 0 && scroll_offset.x() > 0) {
stretch_adjustment.set_x(
std::min(-stretch.x(), static_cast<float>(scroll_offset.x())));
}
if (stretch.x() > 0 && scroll_offset.x() < max_scroll_offset.x()) {
stretch_adjustment.set_x(std::max(
-stretch.x(),
static_cast<float>(scroll_offset.x() - max_scroll_offset.x())));
}
if (stretch.y() < 0 && scroll_offset.y() > 0) {
stretch_adjustment.set_y(
std::min(-stretch.y(), static_cast<float>(scroll_offset.y())));
}
if (stretch.y() > 0 && scroll_offset.y() < max_scroll_offset.y()) {
stretch_adjustment.set_y(std::max(
-stretch.y(),
static_cast<float>(scroll_offset.y() - max_scroll_offset.y())));
}
if (stretch_adjustment.IsZero())
return;
gfx::Vector2dF new_stretch_amount = stretch + stretch_adjustment;
helper_->ScrollBy(-stretch_adjustment);
helper_->SetStretchAmount(new_stretch_amount);
// Update the internal state for the active scroll to avoid discontinuities.
if (state_ == kStateActiveScroll) {
stretch_scroll_force_ =
AccumulatedOverscrollForStretchAmount(new_stretch_amount);
}
}
} // namespace blink