blob: 6f239d2790f40a905d68759b293de8f51c883569 [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 "ui/events/blink/input_scroll_elasticity_controller.h"
#include "cc/input/input_handler.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/WebKit/public/platform/WebInputEvent.h"
#include "third_party/WebKit/public/platform/WebMouseWheelEvent.h"
namespace ui {
namespace {
enum Phase {
PhaseNone = blink::WebMouseWheelEvent::kPhaseNone,
PhaseBegan = blink::WebMouseWheelEvent::kPhaseBegan,
PhaseStationary = blink::WebMouseWheelEvent::kPhaseStationary,
PhaseChanged = blink::WebMouseWheelEvent::kPhaseChanged,
PhaseEnded = blink::WebMouseWheelEvent::kPhaseEnded,
PhaseCancelled = blink::WebMouseWheelEvent::kPhaseCancelled,
PhaseMayBegin = blink::WebMouseWheelEvent::kPhaseMayBegin,
};
enum InertialPhaseState {
UnknownMomentumPhase =
blink::WebGestureEvent::InertialPhaseState::kUnknownMomentumPhase,
NonMomentumPhase =
blink::WebGestureEvent::InertialPhaseState::kNonMomentumPhase,
MomentumPhase = blink::WebGestureEvent::InertialPhaseState::kMomentumPhase,
};
class MockScrollElasticityHelper : public cc::ScrollElasticityHelper {
public:
MockScrollElasticityHelper()
: is_user_scrollable_(true),
set_stretch_amount_count_(0),
request_begin_frame_count_(0) {}
~MockScrollElasticityHelper() override {}
// cc::ScrollElasticityHelper implementation:
bool IsUserScrollable() const override { return is_user_scrollable_; }
gfx::Vector2dF StretchAmount() const override { return stretch_amount_; }
void SetStretchAmount(const gfx::Vector2dF& stretch_amount) override {
set_stretch_amount_count_ += 1;
stretch_amount_ = stretch_amount;
}
gfx::ScrollOffset ScrollOffset() const override { return scroll_offset_; }
gfx::ScrollOffset MaxScrollOffset() const override {
return max_scroll_offset_;
}
void ScrollBy(const gfx::Vector2dF& delta) override {
scroll_offset_ += gfx::ScrollOffset(delta);
}
void RequestOneBeginFrame() override { request_begin_frame_count_ += 1; }
// Counters for number of times functions were called.
int request_begin_frame_count() const { return request_begin_frame_count_; }
int set_stretch_amount_count() const { return set_stretch_amount_count_; }
void SetScrollOffsetAndMaxScrollOffset(
const gfx::ScrollOffset& scroll_offset,
const gfx::ScrollOffset& max_scroll_offset) {
scroll_offset_ = scroll_offset;
max_scroll_offset_ = max_scroll_offset;
}
void SetUserScrollable(bool is_user_scrollable) {
is_user_scrollable_ = is_user_scrollable;
}
private:
bool is_user_scrollable_;
gfx::Vector2dF stretch_amount_;
int set_stretch_amount_count_;
int request_begin_frame_count_;
gfx::ScrollOffset scroll_offset_;
gfx::ScrollOffset max_scroll_offset_;
};
class ScrollElasticityControllerTest : public testing::Test {
public:
ScrollElasticityControllerTest()
: controller_(&helper_),
input_event_count_(0),
current_time_(base::TimeTicks::FromInternalValue(100000000ull)) {}
~ScrollElasticityControllerTest() override {}
void SendGestureScrollBegin(InertialPhaseState inertialPhase) {
TickCurrentTime();
blink::WebGestureEvent event(
blink::WebInputEvent::kGestureScrollBegin,
blink::WebInputEvent::kNoModifiers,
(current_time_ - base::TimeTicks()).InSecondsF());
event.source_device = blink::kWebGestureDeviceTouchpad;
event.data.scroll_begin.inertial_phase =
static_cast<blink::WebGestureEvent::InertialPhaseState>(inertialPhase);
controller_.ObserveGestureEventAndResult(event,
cc::InputHandlerScrollResult());
input_event_count_ += 1;
}
void SendGestureScrollUpdate(
InertialPhaseState inertialPhase,
const gfx::Vector2dF& event_delta = gfx::Vector2dF(),
const gfx::Vector2dF& overscroll_delta = gfx::Vector2dF()) {
TickCurrentTime();
blink::WebGestureEvent event(
blink::WebInputEvent::kGestureScrollUpdate,
blink::WebInputEvent::kNoModifiers,
(current_time_ - base::TimeTicks()).InSecondsF());
event.source_device = blink::kWebGestureDeviceTouchpad;
event.data.scroll_update.inertial_phase =
static_cast<blink::WebGestureEvent::InertialPhaseState>(inertialPhase);
event.data.scroll_update.delta_x = -event_delta.x();
event.data.scroll_update.delta_y = -event_delta.y();
cc::InputHandlerScrollResult scroll_result;
scroll_result.did_overscroll_root = !overscroll_delta.IsZero();
scroll_result.unused_scroll_delta = overscroll_delta;
controller_.ObserveGestureEventAndResult(event, scroll_result);
input_event_count_ += 1;
}
void SendGestureScrollEnd() {
TickCurrentTime();
blink::WebGestureEvent event(
blink::WebInputEvent::kGestureScrollEnd,
blink::WebInputEvent::kNoModifiers,
(current_time_ - base::TimeTicks()).InSecondsF());
event.source_device = blink::kWebGestureDeviceTouchpad;
controller_.ObserveGestureEventAndResult(event,
cc::InputHandlerScrollResult());
input_event_count_ += 1;
}
const base::TimeTicks& TickCurrentTime() {
current_time_ += base::TimeDelta::FromSecondsD(1 / 60.f);
return current_time_;
}
void TickCurrentTimeAndAnimate() {
TickCurrentTime();
controller_.Animate(current_time_);
}
MockScrollElasticityHelper helper_;
InputScrollElasticityController controller_;
int input_event_count_;
base::TimeTicks current_time_;
};
// Verify that stretching occurs in one axis at a time, and that it
// is biased to the Y axis.
TEST_F(ScrollElasticityControllerTest, Axis) {
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(0, 0),
gfx::ScrollOffset(0, 0));
// If we push equally in the X and Y directions, we should see a stretch
// in the Y direction.
SendGestureScrollBegin(NonMomentumPhase);
SendGestureScrollUpdate(NonMomentumPhase, gfx::Vector2dF(10, 10),
gfx::Vector2dF(10, 10));
EXPECT_EQ(1, helper_.set_stretch_amount_count());
EXPECT_EQ(0.f, helper_.StretchAmount().x());
EXPECT_LT(0.f, helper_.StretchAmount().y());
helper_.SetStretchAmount(gfx::Vector2dF());
EXPECT_EQ(2, helper_.set_stretch_amount_count());
SendGestureScrollEnd();
EXPECT_EQ(0, helper_.request_begin_frame_count());
// If we push more in the X direction than the Y direction, we should see a
// stretch in the X direction. This decision should be based on the
// input delta, not the actual overscroll delta.
SendGestureScrollBegin(NonMomentumPhase);
SendGestureScrollUpdate(NonMomentumPhase, gfx::Vector2dF(-25, 10),
gfx::Vector2dF(-25, 40));
EXPECT_EQ(3, helper_.set_stretch_amount_count());
EXPECT_GT(0.f, helper_.StretchAmount().x());
EXPECT_EQ(0.f, helper_.StretchAmount().y());
helper_.SetStretchAmount(gfx::Vector2dF());
EXPECT_EQ(4, helper_.set_stretch_amount_count());
SendGestureScrollEnd();
EXPECT_EQ(0, helper_.request_begin_frame_count());
}
// Verify that we need a total overscroll delta of at least 10 in a pinned
// direction before we start stretching.
TEST_F(ScrollElasticityControllerTest, MinimumDeltaBeforeStretch) {
// We should not start stretching while we are not pinned in the direction
// of the scroll (even if there is an overscroll delta). We have to wait for
// the regular scroll to eat all of the events.
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(5, 5),
gfx::ScrollOffset(10, 10));
SendGestureScrollBegin(NonMomentumPhase);
SendGestureScrollUpdate(NonMomentumPhase, gfx::Vector2dF(0, 10),
gfx::Vector2dF(0, 10));
SendGestureScrollUpdate(NonMomentumPhase, gfx::Vector2dF(0, 10),
gfx::Vector2dF(0, 10));
EXPECT_EQ(0, helper_.set_stretch_amount_count());
// Now pin the -X and +Y direction. The first event will not generate a
// stretch
// because it is below the delta threshold of 10.
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(0, 10),
gfx::ScrollOffset(10, 10));
SendGestureScrollUpdate(NonMomentumPhase, gfx::Vector2dF(0, 10),
gfx::Vector2dF(0, 8));
EXPECT_EQ(0, helper_.set_stretch_amount_count());
// Make the next scroll be in the -X direction more than the +Y direction,
// which will erase the memory of the previous unused delta of 8.
SendGestureScrollUpdate(NonMomentumPhase, gfx::Vector2dF(-10, 5),
gfx::Vector2dF(-8, 10));
EXPECT_EQ(0, helper_.set_stretch_amount_count());
// Now push against the pinned +Y direction again by 8. We reset the
// previous delta, so this will not generate a stretch.
SendGestureScrollUpdate(NonMomentumPhase, gfx::Vector2dF(0, 10),
gfx::Vector2dF(0, 8));
EXPECT_EQ(0, helper_.set_stretch_amount_count());
// Push against +Y by another 8. This gets us above the delta threshold of
// 10, so we should now have had the stretch set, and it should be in the
// +Y direction. The scroll in the -X direction should have been forgotten.
SendGestureScrollUpdate(NonMomentumPhase, gfx::Vector2dF(0, 10),
gfx::Vector2dF(0, 8));
EXPECT_EQ(1, helper_.set_stretch_amount_count());
EXPECT_EQ(0.f, helper_.StretchAmount().x());
EXPECT_LT(0.f, helper_.StretchAmount().y());
// End the gesture. Because there is a non-zero stretch, we should be in the
// animated state, and should have had a frame requested.
EXPECT_EQ(0, helper_.request_begin_frame_count());
SendGestureScrollEnd();
EXPECT_EQ(1, helper_.request_begin_frame_count());
}
// Verify that a stretch caused by a momentum scroll will switch to the
// animating mode, where input events are ignored, and the stretch is updated
// while animating.
TEST_F(ScrollElasticityControllerTest, MomentumAnimate) {
// Do an active scroll, then switch to the momentum phase and scroll for a
// bit.
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(5, 5),
gfx::ScrollOffset(10, 10));
SendGestureScrollBegin(NonMomentumPhase);
SendGestureScrollUpdate(NonMomentumPhase, gfx::Vector2dF(0, -80),
gfx::Vector2dF(0, 0));
SendGestureScrollUpdate(NonMomentumPhase, gfx::Vector2dF(0, -80),
gfx::Vector2dF(0, 0));
SendGestureScrollUpdate(NonMomentumPhase, gfx::Vector2dF(0, -80),
gfx::Vector2dF(0, 0));
SendGestureScrollEnd();
SendGestureScrollBegin(MomentumPhase);
SendGestureScrollUpdate(MomentumPhase, gfx::Vector2dF(0, -80),
gfx::Vector2dF(0, 0));
SendGestureScrollUpdate(MomentumPhase, gfx::Vector2dF(0, -80),
gfx::Vector2dF(0, 0));
SendGestureScrollUpdate(MomentumPhase, gfx::Vector2dF(0, -80),
gfx::Vector2dF(0, 0));
EXPECT_EQ(0, helper_.set_stretch_amount_count());
// Hit the -Y edge and overscroll slightly, but not enough to go over the
// threshold to cause a stretch.
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(5, 0),
gfx::ScrollOffset(10, 10));
SendGestureScrollUpdate(MomentumPhase, gfx::Vector2dF(0, -80),
gfx::Vector2dF(0, -8));
EXPECT_EQ(0, helper_.set_stretch_amount_count());
EXPECT_EQ(0, helper_.request_begin_frame_count());
// Take another step, this time going over the threshold. This should update
// the stretch amount, and then switch to the animating mode.
SendGestureScrollUpdate(MomentumPhase, gfx::Vector2dF(0, -80),
gfx::Vector2dF(0, -80));
EXPECT_EQ(1, helper_.set_stretch_amount_count());
EXPECT_EQ(1, helper_.request_begin_frame_count());
EXPECT_GT(-1.f, helper_.StretchAmount().y());
// Subsequent momentum events should do nothing.
SendGestureScrollUpdate(MomentumPhase, gfx::Vector2dF(0, -80),
gfx::Vector2dF(0, -80));
SendGestureScrollUpdate(MomentumPhase, gfx::Vector2dF(0, -80),
gfx::Vector2dF(0, -80));
SendGestureScrollUpdate(MomentumPhase, gfx::Vector2dF(0, -80),
gfx::Vector2dF(0, -80));
SendGestureScrollEnd();
EXPECT_EQ(1, helper_.set_stretch_amount_count());
EXPECT_EQ(1, helper_.request_begin_frame_count());
// Subsequent animate events should update the stretch amount and request
// another frame.
TickCurrentTimeAndAnimate();
EXPECT_EQ(2, helper_.set_stretch_amount_count());
EXPECT_EQ(2, helper_.request_begin_frame_count());
EXPECT_GT(-1.f, helper_.StretchAmount().y());
// Touching the trackpad (a PhaseMayBegin event) should disable animation.
SendGestureScrollBegin(NonMomentumPhase);
TickCurrentTimeAndAnimate();
EXPECT_EQ(2, helper_.set_stretch_amount_count());
EXPECT_EQ(2, helper_.request_begin_frame_count());
// Releasing the trackpad should re-enable animation.
SendGestureScrollEnd();
EXPECT_EQ(2, helper_.set_stretch_amount_count());
EXPECT_EQ(3, helper_.request_begin_frame_count());
TickCurrentTimeAndAnimate();
EXPECT_EQ(3, helper_.set_stretch_amount_count());
EXPECT_EQ(4, helper_.request_begin_frame_count());
// Keep animating frames until the stretch returns to rest.
int stretch_count = 3;
int begin_frame_count = 4;
while (1) {
TickCurrentTimeAndAnimate();
if (helper_.StretchAmount().IsZero()) {
stretch_count += 1;
EXPECT_EQ(stretch_count, helper_.set_stretch_amount_count());
EXPECT_EQ(begin_frame_count, helper_.request_begin_frame_count());
break;
}
stretch_count += 1;
begin_frame_count += 1;
EXPECT_EQ(stretch_count, helper_.set_stretch_amount_count());
EXPECT_EQ(begin_frame_count, helper_.request_begin_frame_count());
}
// After coming to rest, no subsequent animate calls change anything.
TickCurrentTimeAndAnimate();
EXPECT_EQ(stretch_count, helper_.set_stretch_amount_count());
EXPECT_EQ(begin_frame_count, helper_.request_begin_frame_count());
}
// Verify that a stretch opposing a scroll is correctly resolved.
TEST_F(ScrollElasticityControllerTest, ReconcileStretchAndScroll) {
SendGestureScrollBegin(NonMomentumPhase);
// Verify completely knocking out the scroll in the -Y direction.
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(5, 5),
gfx::ScrollOffset(10, 10));
helper_.SetStretchAmount(gfx::Vector2dF(0, -10));
controller_.ReconcileStretchAndScroll();
EXPECT_EQ(helper_.StretchAmount(), gfx::Vector2dF(0, -5));
EXPECT_EQ(helper_.ScrollOffset(), gfx::ScrollOffset(5, 0));
// Verify partially knocking out the scroll in the -Y direction.
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(5, 8),
gfx::ScrollOffset(10, 10));
helper_.SetStretchAmount(gfx::Vector2dF(0, -5));
controller_.ReconcileStretchAndScroll();
EXPECT_EQ(helper_.StretchAmount(), gfx::Vector2dF(0, 0));
EXPECT_EQ(helper_.ScrollOffset(), gfx::ScrollOffset(5, 3));
// Verify completely knocking out the scroll in the +X direction.
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(5, 5),
gfx::ScrollOffset(10, 10));
helper_.SetStretchAmount(gfx::Vector2dF(10, 0));
controller_.ReconcileStretchAndScroll();
EXPECT_EQ(helper_.StretchAmount(), gfx::Vector2dF(5, 0));
EXPECT_EQ(helper_.ScrollOffset(), gfx::ScrollOffset(10, 5));
// Verify partially knocking out the scroll in the +X and +Y directions.
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(2, 3),
gfx::ScrollOffset(10, 10));
helper_.SetStretchAmount(gfx::Vector2dF(5, 5));
controller_.ReconcileStretchAndScroll();
EXPECT_EQ(helper_.StretchAmount(), gfx::Vector2dF(0, 0));
EXPECT_EQ(helper_.ScrollOffset(), gfx::ScrollOffset(7, 8));
}
// Verify that stretching happens when the area is user scrollable.
TEST_F(ScrollElasticityControllerTest,
UserScrollableRequiredForStretch) {
helper_.SetScrollOffsetAndMaxScrollOffset(gfx::ScrollOffset(0, 0),
gfx::ScrollOffset(10, 10));
gfx::Vector2dF delta(0, -15);
// Do an active scroll, and ensure that the stretch amount doesn't change.
helper_.SetUserScrollable(false);
SendGestureScrollBegin(NonMomentumPhase);
SendGestureScrollUpdate(NonMomentumPhase, delta, delta);
SendGestureScrollUpdate(NonMomentumPhase, delta, delta);
SendGestureScrollEnd();
EXPECT_EQ(helper_.StretchAmount(), gfx::Vector2dF(0, 0));
EXPECT_EQ(0, helper_.set_stretch_amount_count());
SendGestureScrollBegin(MomentumPhase);
SendGestureScrollUpdate(MomentumPhase, delta, delta);
SendGestureScrollUpdate(MomentumPhase, delta, delta);
SendGestureScrollEnd();
EXPECT_EQ(helper_.StretchAmount(), gfx::Vector2dF(0, 0));
EXPECT_EQ(0, helper_.set_stretch_amount_count());
// Re-enable user scrolling and ensure that stretching is re-enabled.
helper_.SetUserScrollable(true);
SendGestureScrollBegin(NonMomentumPhase);
SendGestureScrollUpdate(NonMomentumPhase, delta, delta);
SendGestureScrollUpdate(NonMomentumPhase, delta, delta);
SendGestureScrollEnd();
EXPECT_NE(helper_.StretchAmount(), gfx::Vector2dF(0, 0));
EXPECT_GT(helper_.set_stretch_amount_count(), 0);
SendGestureScrollBegin(MomentumPhase);
SendGestureScrollUpdate(MomentumPhase, delta, delta);
SendGestureScrollUpdate(MomentumPhase, delta, delta);
SendGestureScrollEnd();
EXPECT_NE(helper_.StretchAmount(), gfx::Vector2dF(0, 0));
EXPECT_GT(helper_.set_stretch_amount_count(), 0);
// Disable user scrolling and tick the timer until the stretch goes back
// to zero. Ensure that the return to zero doesn't happen immediately.
helper_.SetUserScrollable(false);
int ticks_to_zero = 0;
while (1) {
TickCurrentTimeAndAnimate();
if (helper_.StretchAmount().IsZero())
break;
ticks_to_zero += 1;
}
EXPECT_GT(ticks_to_zero, 3);
}
} // namespace
} // namespace ui