blob: d39405091d1bef6ec7a047813bc51290f1b61abb [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/views/animation/slide_out_controller.h"
#include <memory>
#include <utility>
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "ui/base/ui_base_features.h"
#include "ui/compositor/layer.h"
#include "ui/events/event.h"
#include "ui/events/test/event_generator.h"
#include "ui/events/types/event_type.h"
#include "ui/views/animation/slide_out_controller_delegate.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/view.h"
namespace views {
namespace {
constexpr int kSwipeControlWidth = 30; // px
constexpr int kTargetWidth = 200; // px
} // namespace
class TestSlideOutControllerDelegate : public SlideOutControllerDelegate {
public:
explicit TestSlideOutControllerDelegate(View* target) : target_(target) {}
~TestSlideOutControllerDelegate() override = default;
ui::Layer* GetSlideOutLayer() override { return target_->layer(); }
void OnSlideStarted() override { ++slide_started_count_; }
void OnSlideChanged(bool in_progress) override {
slide_changed_last_value_ = in_progress;
++slide_changed_count_;
}
bool IsOnSlideChangedCalled() const { return (slide_changed_count_ > 0); }
void OnSlideOut() override { ++slide_out_count_; }
void reset() {
slide_started_count_ = 0;
slide_changed_count_ = 0;
slide_out_count_ = 0;
slide_changed_last_value_ = std::nullopt;
}
std::optional<bool> slide_changed_last_value_;
int slide_started_count_ = 0;
int slide_changed_count_ = 0;
int slide_out_count_ = 0;
private:
const raw_ptr<View> target_;
};
class SlideOutControllerTest : public ViewsTestBase {
public:
SlideOutControllerTest() = default;
~SlideOutControllerTest() override = default;
void SetUp() override {
ViewsTestBase::SetUp();
widget_ = std::make_unique<Widget>();
Widget::InitParams params = CreateParams(
Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP);
params.bounds = gfx::Rect(50, 50, 650, 650);
widget_->Init(std::move(params));
View* root = widget_->GetRootView();
View* target = root->AddChildView(std::make_unique<View>());
target->SetPaintToLayer(ui::LAYER_TEXTURED);
target->SetSize(gfx::Size(kTargetWidth, 50));
widget_->Show();
delegate_ = std::make_unique<TestSlideOutControllerDelegate>(target);
slide_out_controller_ =
std::make_unique<SlideOutController>(target, delegate_.get());
}
void TearDown() override {
slide_out_controller_.reset();
delegate_.reset();
widget_.reset();
ViewsTestBase::TearDown();
}
protected:
SlideOutController* slide_out_controller() {
return slide_out_controller_.get();
}
TestSlideOutControllerDelegate* delegate() { return delegate_.get(); }
void PostSequentialGestureEvent(const ui::GestureEventDetails& details) {
// Set the timestamp ahead one microsecond.
sequential_event_timestamp_ += base::Microseconds(1);
ui::GestureEvent gesture_event(
0, 0, ui::EF_NONE, base::TimeTicks() + sequential_event_timestamp_,
details);
slide_out_controller()->OnGestureEvent(&gesture_event);
}
void PostSequentialSwipeEvent(int swipe_amount) {
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollBegin));
PostSequentialGestureEvent(ui::GestureEventDetails(
ui::EventType::kGestureScrollUpdate, swipe_amount, 0));
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollEnd));
}
void PostTrackPadSwipeEvent(ui::EventType type,
int swipe_amount,
int finger_count) {
auto scroll_event =
ui::ScrollEvent(type, gfx::PointF(), gfx::PointF(), base::TimeTicks(),
0, swipe_amount, 0, 0, 0, finger_count);
slide_out_controller()->OnScrollEvent(&scroll_event);
}
private:
std::unique_ptr<Widget> widget_;
std::unique_ptr<SlideOutController> slide_out_controller_;
std::unique_ptr<TestSlideOutControllerDelegate> delegate_;
base::TimeDelta sequential_event_timestamp_;
};
TEST_F(SlideOutControllerTest, OnGestureEventAndDelegate) {
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollBegin));
EXPECT_EQ(1, delegate()->slide_started_count_);
EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(0, delegate()->slide_out_count_);
delegate()->reset();
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollUpdate));
EXPECT_EQ(0, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
delegate()->reset();
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollEnd));
EXPECT_EQ(0, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_FALSE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
}
TEST_F(SlideOutControllerTest, SlideOutAndClose) {
// Place a finger on notification.
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollBegin));
EXPECT_EQ(1, delegate()->slide_started_count_);
EXPECT_EQ(0, delegate()->slide_changed_count_);
EXPECT_EQ(0, delegate()->slide_out_count_);
delegate()->reset();
// Move the finger horizontally by 101 px. (101 px is more than half of the
// target width 200 px)
PostSequentialGestureEvent(ui::GestureEventDetails(
ui::EventType::kGestureScrollUpdate, kTargetWidth / 2 + 1, 0));
EXPECT_EQ(0, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
delegate()->reset();
// Release the finger.
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollEnd));
EXPECT_EQ(0, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_FALSE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
// The target has been scrolled out and the current location is moved by the
// width (200px).
EXPECT_EQ(kTargetWidth,
delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
delegate()->reset();
// Ensure a deferred OnSlideOut handler is called.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(1, delegate()->slide_out_count_);
}
TEST_F(SlideOutControllerTest, SlideLittleAmountAndNotClose) {
// Place a finger on notification.
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollBegin));
EXPECT_EQ(1, delegate()->slide_started_count_);
EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(0, delegate()->slide_out_count_);
delegate()->reset();
// Move the finger horizontally by 99 px. (99 px is less than half of the
// target width 200 px)
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollUpdate, 99, 0));
EXPECT_EQ(0, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
delegate()->reset();
// Release the finger.
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollEnd));
EXPECT_EQ(0, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_FALSE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
// The target has been moved back to the origin.
EXPECT_EQ(0.f,
delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
delegate()->reset();
// Ensure no deferred SlideOut handler.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(0, delegate()->slide_out_count_);
}
class SwipeControlTest
: public SlideOutControllerTest,
public testing::WithParamInterface<SlideOutController::SlideMode> {
public:
SwipeControlTest() = default;
SwipeControlTest(const SwipeControlTest&) = delete;
SwipeControlTest& operator=(const SwipeControlTest&) = delete;
~SwipeControlTest() override = default;
void SetUp() override {
SlideOutControllerTest::SetUp();
slide_out_controller()->set_slide_mode(GetParam());
}
};
INSTANTIATE_TEST_SUITE_P(
All,
SwipeControlTest,
::testing::Values(SlideOutController::SlideMode::kFull,
SlideOutController::SlideMode::kPartial));
TEST_P(SwipeControlTest, SetSwipeControlWidth_SwipeLessThanControlWidth) {
// Set the width of swipe control.
slide_out_controller()->SetSwipeControlWidth(kSwipeControlWidth);
// Place a finger on notification.
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollBegin));
EXPECT_EQ(1, delegate()->slide_started_count_);
EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(0, delegate()->slide_out_count_);
delegate()->reset();
// Move the finger horizontally by 29 px. (29 px is less than the swipe
// control width).
PostSequentialGestureEvent(ui::GestureEventDetails(
ui::EventType::kGestureScrollUpdate, kSwipeControlWidth - 1, 0));
EXPECT_EQ(0, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
delegate()->reset();
// Release the finger.
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollEnd));
EXPECT_EQ(0, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_FALSE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
// The target has been moved back to the origin.
EXPECT_EQ(0.f,
delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
delegate()->reset();
// Ensure no deferred SlideOut handler.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(0, delegate()->slide_out_count_);
}
TEST_P(SwipeControlTest, SwipeControlWidth_SwipeMoreThanControlWidth) {
// Set the width of swipe control.
slide_out_controller()->SetSwipeControlWidth(kSwipeControlWidth);
// Place a finger on notification.
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollBegin));
EXPECT_EQ(1, delegate()->slide_started_count_);
EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(0, delegate()->slide_out_count_);
delegate()->reset();
// Move the finger horizontally by 31 px. (31 px is more than the swipe
// control width).
PostSequentialGestureEvent(ui::GestureEventDetails(
ui::EventType::kGestureScrollUpdate, kSwipeControlWidth + 1, 0));
EXPECT_EQ(0, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
delegate()->reset();
// Release the finger.
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollEnd));
EXPECT_EQ(0, delegate()->slide_started_count_);
// Slide is in progress.
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
// Swipe amount is the swipe control width.
EXPECT_EQ(kSwipeControlWidth,
delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
delegate()->reset();
// Ensure no deferred SlideOut handler.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(0, delegate()->slide_out_count_);
}
TEST_P(SwipeControlTest, SetSwipeControlWidth_SwipeOut) {
const bool swipe_out_supported =
slide_out_controller()->mode() == SlideOutController::SlideMode::kFull;
// Set the width of swipe control.
slide_out_controller()->SetSwipeControlWidth(kSwipeControlWidth);
// Place a finger on notification.
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollBegin));
EXPECT_EQ(1, delegate()->slide_started_count_);
EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(0, delegate()->slide_out_count_);
delegate()->reset();
// Move the finger horizontally by 101 px. (101 px is more than the half of
// the target width).
PostSequentialGestureEvent(ui::GestureEventDetails(
ui::EventType::kGestureScrollUpdate, kTargetWidth / 2 + 1, 0));
EXPECT_EQ(0, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
delegate()->reset();
// Release the finger.
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollEnd));
// ... and it is automatically slided out if |swipe_out_supported|.
EXPECT_EQ(0, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(delegate()->slide_changed_last_value_.value(),
!swipe_out_supported);
EXPECT_EQ(0, delegate()->slide_out_count_);
EXPECT_EQ(swipe_out_supported ? kTargetWidth : kSwipeControlWidth,
delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
delegate()->reset();
// Ensure a deferred SlideOut handler is called once.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(swipe_out_supported ? 1 : 0, delegate()->slide_out_count_);
}
TEST_P(SwipeControlTest, SwipeControlWidth_SnapAndSwipeOut) {
const bool swipe_out_supported =
slide_out_controller()->mode() == SlideOutController::SlideMode::kFull;
// Set the width of swipe control.
slide_out_controller()->SetSwipeControlWidth(kSwipeControlWidth);
// Snap to the swipe control.
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollBegin));
PostSequentialGestureEvent(ui::GestureEventDetails(
ui::EventType::kGestureScrollUpdate, kSwipeControlWidth, 0));
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollEnd));
EXPECT_EQ(1, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
EXPECT_EQ(kSwipeControlWidth,
delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
delegate()->reset();
// Swipe horizontally by 70 px.
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollBegin));
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollUpdate, 70, 0));
PostSequentialGestureEvent(
ui::GestureEventDetails(ui::EventType::kGestureScrollEnd));
// ... and it is automatically slided out if if |swipe_out_supported|.
EXPECT_EQ(1, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(delegate()->slide_changed_last_value_.value(),
!swipe_out_supported);
EXPECT_EQ(0, delegate()->slide_out_count_);
EXPECT_EQ(swipe_out_supported ? kTargetWidth : kSwipeControlWidth,
delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
delegate()->reset();
// Ensure a deferred OnSlideOut handler is called.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(swipe_out_supported ? 1 : 0, delegate()->slide_out_count_);
}
TEST_P(SwipeControlTest, SwipeControlWidth_SnapAndSnapToControl) {
// Set the width of swipe control.
slide_out_controller()->SetSwipeControlWidth(kSwipeControlWidth);
// Snap to the swipe control.
PostSequentialSwipeEvent(kSwipeControlWidth + 10);
EXPECT_EQ(1, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
EXPECT_EQ(kSwipeControlWidth,
delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
delegate()->reset();
// Swipe horizontally by 40 px for the same direction.
PostSequentialSwipeEvent(40);
// Snap automatically back to the swipe control.
EXPECT_EQ(1, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
EXPECT_EQ(kSwipeControlWidth,
delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
delegate()->reset();
// Ensure no deferred OnSlideOut handler.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(0, delegate()->slide_out_count_);
}
TEST_P(SwipeControlTest, SwipeControlWidth_SnapAndBackToOrigin) {
// Set the width of swipe control.
slide_out_controller()->SetSwipeControlWidth(kSwipeControlWidth);
// Snap to the swipe control.
PostSequentialSwipeEvent(kSwipeControlWidth + 20);
EXPECT_EQ(1, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
EXPECT_EQ(kSwipeControlWidth,
delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
delegate()->reset();
// Swipe to the reversed direction by -1 px.
PostSequentialSwipeEvent(-1);
// Snap automatically back to the origin.
EXPECT_EQ(1, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_FALSE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
EXPECT_EQ(0,
delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
delegate()->reset();
// Ensure no deferred OnSlideOut handler.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(0, delegate()->slide_out_count_);
}
TEST_P(SwipeControlTest, SwipeControlWidth_NotSnapAndBackToOrigin) {
// Set the width of swipe control.
slide_out_controller()->SetSwipeControlWidth(kSwipeControlWidth);
// Swipe partially but it's not enough to snap to the swipe control. So it is
// back to the origin
PostSequentialSwipeEvent(kSwipeControlWidth - 1);
EXPECT_EQ(1, delegate()->slide_started_count_);
EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
EXPECT_FALSE(delegate()->slide_changed_last_value_.value());
EXPECT_EQ(0, delegate()->slide_out_count_);
EXPECT_EQ(0,
delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
delegate()->reset();
// Ensure no deferred OnSlideOut handler.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
EXPECT_EQ(0, delegate()->slide_out_count_);
}
// Test class for making sure trackpad gestures work to trigger slide out
// events.
class TrackPadGestureTest : public SlideOutControllerTest {
public:
TrackPadGestureTest() = default;
explicit TrackPadGestureTest(const SwipeControlTest&) = delete;
TrackPadGestureTest& operator=(const TrackPadGestureTest&) = delete;
~TrackPadGestureTest() override = default;
void SetUp() override {
SlideOutControllerTest::SetUp();
slide_out_controller()->set_trackpad_gestures_enabled(true);
}
};
TEST_F(TrackPadGestureTest, SlideOut) {
int width = delegate()->GetSlideOutLayer()->bounds().width();
// A slide out should not be triggered if the scroll offset isn't less greater
// than the view's width.
PostTrackPadSwipeEvent(ui::EventType::kScroll, width,
/*finger_count=*/2);
PostTrackPadSwipeEvent(ui::EventType::kScrollFlingStart, 0, 2);
EXPECT_EQ(0, delegate()->slide_out_count_);
// A slide out should not be triggered if the finger count is not equal to 2.
PostTrackPadSwipeEvent(ui::EventType::kScroll, width + 1,
/*finger_count=*/3);
PostTrackPadSwipeEvent(ui::EventType::kScrollFlingStart, 0,
/*finger_count=*/3);
EXPECT_EQ(0, delegate()->slide_out_count_);
PostTrackPadSwipeEvent(ui::EventType::kScroll, width + 1,
/*finger_count=*/2);
// A slide out should not be triggered until the
// `EventType::kScrollFlingStart` is posted.
EXPECT_EQ(0, delegate()->slide_out_count_);
PostTrackPadSwipeEvent(ui::EventType::kScrollFlingStart, 0, 2);
EXPECT_EQ(1, delegate()->slide_out_count_);
}
} // namespace views