904 lines
36 KiB
C++
904 lines
36 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* 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 "APZCTreeManagerTester.h"
|
|
#include "APZTestCommon.h"
|
|
#include "InputUtils.h"
|
|
|
|
class APZScrollHandoffTester : public APZCTreeManagerTester {
|
|
protected:
|
|
UniquePtr<ScopedLayerTreeRegistration> registration;
|
|
TestAsyncPanZoomController* rootApzc;
|
|
|
|
void CreateScrollHandoffLayerTree1() {
|
|
const char* treeShape = "x(x)";
|
|
LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100),
|
|
LayerIntRect(0, 50, 100, 50)};
|
|
CreateScrollData(treeShape, layerVisibleRect);
|
|
SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
|
|
CSSRect(0, 0, 200, 200));
|
|
SetScrollableFrameMetrics(layers[1],
|
|
ScrollableLayerGuid::START_SCROLL_ID + 1,
|
|
CSSRect(0, 0, 100, 100));
|
|
SetScrollHandoff(layers[1], root);
|
|
registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
|
|
UpdateHitTestingTree();
|
|
rootApzc = ApzcOf(root);
|
|
rootApzc->GetFrameMetrics().SetIsRootContent(
|
|
true); // make root APZC zoomable
|
|
}
|
|
|
|
void CreateScrollHandoffLayerTree2() {
|
|
const char* treeShape = "x(x(x))";
|
|
LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100),
|
|
LayerIntRect(0, 0, 100, 100),
|
|
LayerIntRect(0, 50, 100, 50)};
|
|
CreateScrollData(treeShape, layerVisibleRect);
|
|
SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
|
|
CSSRect(0, 0, 200, 200));
|
|
SetScrollableFrameMetrics(layers[1],
|
|
ScrollableLayerGuid::START_SCROLL_ID + 2,
|
|
CSSRect(-100, -100, 200, 200));
|
|
SetScrollableFrameMetrics(layers[2],
|
|
ScrollableLayerGuid::START_SCROLL_ID + 1,
|
|
CSSRect(0, 0, 100, 100));
|
|
SetScrollHandoff(layers[1], root);
|
|
SetScrollHandoff(layers[2], layers[1]);
|
|
// No ScopedLayerTreeRegistration as that just needs to be done once per
|
|
// test and this is the second layer tree for a particular test.
|
|
MOZ_ASSERT(registration);
|
|
UpdateHitTestingTree();
|
|
rootApzc = ApzcOf(root);
|
|
}
|
|
|
|
void CreateScrollHandoffLayerTree3() {
|
|
const char* treeShape = "x(x(x)x(x))";
|
|
LayerIntRect layerVisibleRect[] = {
|
|
LayerIntRect(0, 0, 100, 100), // root
|
|
LayerIntRect(0, 0, 100, 50), // scrolling parent 1
|
|
LayerIntRect(0, 0, 100, 50), // scrolling child 1
|
|
LayerIntRect(0, 50, 100, 50), // scrolling parent 2
|
|
LayerIntRect(0, 50, 100, 50) // scrolling child 2
|
|
};
|
|
CreateScrollData(treeShape, layerVisibleRect);
|
|
SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID,
|
|
CSSRect(0, 0, 100, 100));
|
|
SetScrollableFrameMetrics(layers[1],
|
|
ScrollableLayerGuid::START_SCROLL_ID + 1,
|
|
CSSRect(0, 0, 100, 100));
|
|
SetScrollableFrameMetrics(layers[2],
|
|
ScrollableLayerGuid::START_SCROLL_ID + 2,
|
|
CSSRect(0, 0, 100, 100));
|
|
SetScrollableFrameMetrics(layers[3],
|
|
ScrollableLayerGuid::START_SCROLL_ID + 3,
|
|
CSSRect(0, 50, 100, 100));
|
|
SetScrollableFrameMetrics(layers[4],
|
|
ScrollableLayerGuid::START_SCROLL_ID + 4,
|
|
CSSRect(0, 50, 100, 100));
|
|
SetScrollHandoff(layers[1], layers[0]);
|
|
SetScrollHandoff(layers[3], layers[0]);
|
|
SetScrollHandoff(layers[2], layers[1]);
|
|
SetScrollHandoff(layers[4], layers[3]);
|
|
registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
|
|
UpdateHitTestingTree();
|
|
}
|
|
|
|
// Creates a layer tree with a parent layer that is only scrollable
|
|
// horizontally, and a child layer that is only scrollable vertically.
|
|
void CreateScrollHandoffLayerTree4() {
|
|
const char* treeShape = "x(x)";
|
|
LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100),
|
|
LayerIntRect(0, 0, 100, 100)};
|
|
CreateScrollData(treeShape, layerVisibleRect);
|
|
SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
|
|
CSSRect(0, 0, 200, 100));
|
|
SetScrollableFrameMetrics(layers[1],
|
|
ScrollableLayerGuid::START_SCROLL_ID + 1,
|
|
CSSRect(0, 0, 100, 200));
|
|
SetScrollHandoff(layers[1], root);
|
|
registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
|
|
UpdateHitTestingTree();
|
|
rootApzc = ApzcOf(root);
|
|
}
|
|
|
|
// Creates a layer tree with a parent layer that is not scrollable, and a
|
|
// child layer that is only scrollable vertically.
|
|
void CreateScrollHandoffLayerTree5() {
|
|
const char* treeShape = "x(x)";
|
|
LayerIntRect layerVisibleRect[] = {
|
|
LayerIntRect(0, 0, 100, 100), // scrolling parent
|
|
LayerIntRect(0, 50, 100, 50) // scrolling child
|
|
};
|
|
CreateScrollData(treeShape, layerVisibleRect);
|
|
SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
|
|
CSSRect(0, 0, 100, 100));
|
|
SetScrollableFrameMetrics(layers[1],
|
|
ScrollableLayerGuid::START_SCROLL_ID + 1,
|
|
CSSRect(0, 0, 100, 200));
|
|
SetScrollHandoff(layers[1], root);
|
|
registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
|
|
UpdateHitTestingTree();
|
|
rootApzc = ApzcOf(root);
|
|
}
|
|
|
|
// Creates a layer tree with a parent layer that is only scrollable
|
|
// vertically, and a child layer that is only scrollable horizontally.
|
|
void CreateScrollHandoffLayerTree6() {
|
|
const char* treeShape = "x(x)";
|
|
LayerIntRect layerVisibleRect[] = {LayerIntRect(0, 0, 100, 100),
|
|
LayerIntRect(0, 0, 100, 1000)};
|
|
CreateScrollData(treeShape, layerVisibleRect);
|
|
SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
|
|
CSSRect(0, 0, 100, 1000));
|
|
SetScrollableFrameMetrics(layers[1],
|
|
ScrollableLayerGuid::START_SCROLL_ID + 1,
|
|
CSSRect(0, 0, 200, 1000));
|
|
SetScrollHandoff(layers[1], root);
|
|
registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
|
|
UpdateHitTestingTree();
|
|
rootApzc = ApzcOf(root);
|
|
}
|
|
|
|
void TestCrossApzcAxisLock() {
|
|
SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 1);
|
|
|
|
CreateScrollHandoffLayerTree1();
|
|
|
|
RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
|
|
Pan(childApzc, ScreenIntPoint(10, 60), ScreenIntPoint(15, 90),
|
|
PanOptions::KeepFingerDown | PanOptions::ExactCoordinates);
|
|
|
|
childApzc->AssertAxisLocked(ScrollDirection::eVertical);
|
|
childApzc->AssertStateIsPanningLockedY();
|
|
}
|
|
};
|
|
|
|
class APZScrollHandoffTesterMock : public APZScrollHandoffTester {
|
|
public:
|
|
APZScrollHandoffTesterMock() { CreateMockHitTester(); }
|
|
};
|
|
|
|
class APZCNestedFlingScrollHandoffTester : public APZCTreeManagerTester {
|
|
protected:
|
|
void SetUp() {
|
|
APZCTreeManagerTester::SetUp();
|
|
const char* treeShape = "x(x)";
|
|
LayerIntRect layerVisibleRect[] = {
|
|
LayerIntRect(0, 0, 800, 1000),
|
|
LayerIntRect(0, 0, 100, 100),
|
|
};
|
|
|
|
CreateScrollData(treeShape, layerVisibleRect);
|
|
|
|
SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
|
|
CSSRect(0, 0, 800, 50000));
|
|
SetScrollableFrameMetrics(layers[1],
|
|
ScrollableLayerGuid::START_SCROLL_ID + 1,
|
|
CSSRect(0, 0, 800, 100));
|
|
|
|
SetScrollHandoff(layers[1], root);
|
|
|
|
// Scroll somewhere into the middle of the scroll range, so that we have
|
|
// lots of space to scroll in both directions.
|
|
ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
|
|
aMetrics.SetVisualScrollUpdateType(
|
|
FrameMetrics::ScrollOffsetUpdateType::eMainThread);
|
|
aMetrics.SetVisualDestination(CSSPoint(0, 25000));
|
|
});
|
|
|
|
registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
|
|
UpdateHitTestingTree();
|
|
|
|
subframeApzc = ApzcOf(layers[1]);
|
|
}
|
|
|
|
void ExecuteDirectionChangingPanGesture(
|
|
const ScreenIntPoint& aStartPoint,
|
|
std::initializer_list<int32_t> aXDeltas,
|
|
std::initializer_list<int32_t> aYDeltas) {
|
|
APZEventResult result = TouchDown(subframeApzc, aStartPoint, mcc->Time());
|
|
|
|
// Allowed touch behaviours must be set after sending touch-start.
|
|
if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) {
|
|
SetDefaultAllowedTouchBehavior(subframeApzc, result.mInputBlockId);
|
|
}
|
|
|
|
const TimeDuration kTouchTimeDelta100Hz =
|
|
TimeDuration::FromMilliseconds(10);
|
|
|
|
ScreenIntPoint currentLocation = aStartPoint;
|
|
for (int32_t delta : aXDeltas) {
|
|
mcc->AdvanceBy(kTouchTimeDelta100Hz);
|
|
if (delta != 0) {
|
|
currentLocation.x += delta;
|
|
Unused << TouchMove(subframeApzc, currentLocation, mcc->Time());
|
|
}
|
|
}
|
|
|
|
ExecuteWait(TimeDuration::FromMilliseconds(255));
|
|
|
|
for (int32_t delta : aYDeltas) {
|
|
mcc->AdvanceBy(kTouchTimeDelta100Hz);
|
|
if (delta != 0) {
|
|
currentLocation.y += delta;
|
|
Unused << TouchMove(subframeApzc, currentLocation, mcc->Time());
|
|
}
|
|
}
|
|
|
|
Unused << TouchUp(subframeApzc, currentLocation, mcc->Time());
|
|
}
|
|
|
|
void ExecuteWait(const TimeDuration& aDuration) {
|
|
TimeDuration remaining = aDuration;
|
|
const TimeDuration TIME_BETWEEN_FRAMES =
|
|
TimeDuration::FromSeconds(1) / int64_t(60);
|
|
while (remaining.ToMilliseconds() > 0) {
|
|
mcc->AdvanceBy(TIME_BETWEEN_FRAMES);
|
|
subframeApzc->AdvanceAnimations(mcc->GetSampleTime());
|
|
remaining -= TIME_BETWEEN_FRAMES;
|
|
}
|
|
}
|
|
|
|
RefPtr<TestAsyncPanZoomController> subframeApzc;
|
|
UniquePtr<ScopedLayerTreeRegistration> registration;
|
|
};
|
|
#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
|
|
// Here we test that if the processing of a touch block is deferred while we
|
|
// wait for content to send a prevent-default message, overscroll is still
|
|
// handed off correctly when the block is processed.
|
|
TEST_F(APZScrollHandoffTester, DeferredInputEventProcessing) {
|
|
SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
|
|
|
|
// Set up the APZC tree.
|
|
CreateScrollHandoffLayerTree1();
|
|
|
|
RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
|
|
|
|
// Enable touch-listeners so that we can separate the queueing of input
|
|
// events from them being processed.
|
|
childApzc->SetWaitForMainThread();
|
|
|
|
// Queue input events for a pan.
|
|
uint64_t blockId = 0;
|
|
Pan(childApzc, 90, 30, PanOptions::NoFling, nullptr, nullptr, &blockId);
|
|
|
|
// Allow the pan to be processed.
|
|
childApzc->ContentReceivedInputBlock(blockId, false);
|
|
childApzc->ConfirmTarget(blockId);
|
|
|
|
// Make sure overscroll was handed off correctly.
|
|
EXPECT_EQ(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y);
|
|
EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y);
|
|
}
|
|
#endif
|
|
|
|
#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
|
|
// Here we test that if the layer structure changes in between two input
|
|
// blocks being queued, and the first block is only processed after the second
|
|
// one has been queued, overscroll handoff for the first block follows
|
|
// the original layer structure while overscroll handoff for the second block
|
|
// follows the new layer structure.
|
|
TEST_F(APZScrollHandoffTester, LayerStructureChangesWhileEventsArePending) {
|
|
SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
|
|
|
|
// Set up an initial APZC tree.
|
|
CreateScrollHandoffLayerTree1();
|
|
|
|
RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
|
|
|
|
// Enable touch-listeners so that we can separate the queueing of input
|
|
// events from them being processed.
|
|
childApzc->SetWaitForMainThread();
|
|
|
|
// Queue input events for a pan.
|
|
uint64_t blockId = 0;
|
|
Pan(childApzc, 90, 30, PanOptions::NoFling, nullptr, nullptr, &blockId);
|
|
|
|
// Modify the APZC tree to insert a new APZC 'middle' into the handoff chain
|
|
// between the child and the root.
|
|
CreateScrollHandoffLayerTree2();
|
|
WebRenderLayerScrollData* middle = layers[1];
|
|
childApzc->SetWaitForMainThread();
|
|
TestAsyncPanZoomController* middleApzc = ApzcOf(middle);
|
|
|
|
// Queue input events for another pan.
|
|
uint64_t secondBlockId = 0;
|
|
Pan(childApzc, 30, 90, PanOptions::NoFling, nullptr, nullptr, &secondBlockId);
|
|
|
|
// Allow the first pan to be processed.
|
|
childApzc->ContentReceivedInputBlock(blockId, false);
|
|
childApzc->ConfirmTarget(blockId);
|
|
|
|
// Make sure things have scrolled according to the handoff chain in
|
|
// place at the time the touch-start of the first pan was queued.
|
|
EXPECT_EQ(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y);
|
|
EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y);
|
|
EXPECT_EQ(0, middleApzc->GetFrameMetrics().GetVisualScrollOffset().y);
|
|
|
|
// Allow the second pan to be processed.
|
|
childApzc->ContentReceivedInputBlock(secondBlockId, false);
|
|
childApzc->ConfirmTarget(secondBlockId);
|
|
|
|
// Make sure things have scrolled according to the handoff chain in
|
|
// place at the time the touch-start of the second pan was queued.
|
|
EXPECT_EQ(0, childApzc->GetFrameMetrics().GetVisualScrollOffset().y);
|
|
EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y);
|
|
EXPECT_EQ(-10, middleApzc->GetFrameMetrics().GetVisualScrollOffset().y);
|
|
}
|
|
#endif
|
|
|
|
#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
|
|
// Test that putting a second finger down on an APZC while a down-chain APZC
|
|
// is overscrolled doesn't result in being stuck in overscroll.
|
|
TEST_F(APZScrollHandoffTesterMock, StuckInOverscroll_Bug1073250) {
|
|
// Enable overscrolling.
|
|
SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
|
|
SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
|
|
|
|
CreateScrollHandoffLayerTree1();
|
|
|
|
TestAsyncPanZoomController* child = ApzcOf(layers[1]);
|
|
|
|
// Pan, causing the parent APZC to overscroll.
|
|
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
|
|
Pan(manager, 10, 40, PanOptions::KeepFingerDown);
|
|
EXPECT_FALSE(child->IsOverscrolled());
|
|
EXPECT_TRUE(rootApzc->IsOverscrolled());
|
|
|
|
// Put a second finger down.
|
|
MultiTouchInput secondFingerDown =
|
|
CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
|
|
// Use the same touch identifier for the first touch (0) as Pan(). (A bit
|
|
// hacky.)
|
|
secondFingerDown.mTouches.AppendElement(
|
|
SingleTouchData(0, ScreenIntPoint(10, 40), ScreenSize(0, 0), 0, 0));
|
|
secondFingerDown.mTouches.AppendElement(
|
|
SingleTouchData(1, ScreenIntPoint(30, 20), ScreenSize(0, 0), 0, 0));
|
|
manager->ReceiveInputEvent(secondFingerDown);
|
|
|
|
// Release the fingers.
|
|
MultiTouchInput fingersUp = secondFingerDown;
|
|
fingersUp.mType = MultiTouchInput::MULTITOUCH_END;
|
|
manager->ReceiveInputEvent(fingersUp);
|
|
|
|
// Allow any animations to run their course.
|
|
child->AdvanceAnimationsUntilEnd();
|
|
rootApzc->AdvanceAnimationsUntilEnd();
|
|
|
|
// Make sure nothing is overscrolled.
|
|
EXPECT_FALSE(child->IsOverscrolled());
|
|
EXPECT_FALSE(rootApzc->IsOverscrolled());
|
|
}
|
|
#endif
|
|
|
|
#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
|
|
// This is almost exactly like StuckInOverscroll_Bug1073250, except the
|
|
// APZC receiving the input events for the first touch block is the child
|
|
// (and thus not the same APZC that overscrolls, which is the parent).
|
|
TEST_F(APZScrollHandoffTesterMock, StuckInOverscroll_Bug1231228) {
|
|
// Enable overscrolling.
|
|
SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
|
|
SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
|
|
|
|
CreateScrollHandoffLayerTree1();
|
|
|
|
TestAsyncPanZoomController* child = ApzcOf(layers[1]);
|
|
|
|
// Pan, causing the parent APZC to overscroll.
|
|
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
|
|
Pan(manager, 60, 90, PanOptions::KeepFingerDown);
|
|
EXPECT_FALSE(child->IsOverscrolled());
|
|
EXPECT_TRUE(rootApzc->IsOverscrolled());
|
|
|
|
// Put a second finger down.
|
|
MultiTouchInput secondFingerDown =
|
|
CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
|
|
// Use the same touch identifier for the first touch (0) as Pan(). (A bit
|
|
// hacky.)
|
|
secondFingerDown.mTouches.AppendElement(
|
|
SingleTouchData(0, ScreenIntPoint(10, 40), ScreenSize(0, 0), 0, 0));
|
|
secondFingerDown.mTouches.AppendElement(
|
|
SingleTouchData(1, ScreenIntPoint(30, 20), ScreenSize(0, 0), 0, 0));
|
|
manager->ReceiveInputEvent(secondFingerDown);
|
|
|
|
// Release the fingers.
|
|
MultiTouchInput fingersUp = secondFingerDown;
|
|
fingersUp.mType = MultiTouchInput::MULTITOUCH_END;
|
|
manager->ReceiveInputEvent(fingersUp);
|
|
|
|
// Allow any animations to run their course.
|
|
child->AdvanceAnimationsUntilEnd();
|
|
rootApzc->AdvanceAnimationsUntilEnd();
|
|
|
|
// Make sure nothing is overscrolled.
|
|
EXPECT_FALSE(child->IsOverscrolled());
|
|
EXPECT_FALSE(rootApzc->IsOverscrolled());
|
|
}
|
|
#endif
|
|
|
|
#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
|
|
TEST_F(APZScrollHandoffTester, StuckInOverscroll_Bug1240202a) {
|
|
// Enable overscrolling.
|
|
SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
|
|
|
|
CreateScrollHandoffLayerTree1();
|
|
|
|
TestAsyncPanZoomController* child = ApzcOf(layers[1]);
|
|
|
|
// Pan, causing the parent APZC to overscroll.
|
|
Pan(manager, 60, 90, PanOptions::KeepFingerDown);
|
|
EXPECT_FALSE(child->IsOverscrolled());
|
|
EXPECT_TRUE(rootApzc->IsOverscrolled());
|
|
|
|
// Lift the finger, triggering an overscroll animation
|
|
// (but don't allow it to run).
|
|
TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time());
|
|
|
|
// Put the finger down again, interrupting the animation
|
|
// and entering the TOUCHING state.
|
|
TouchDown(manager, ScreenIntPoint(10, 90), mcc->Time());
|
|
|
|
// Lift the finger once again.
|
|
TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time());
|
|
|
|
// Allow any animations to run their course.
|
|
child->AdvanceAnimationsUntilEnd();
|
|
rootApzc->AdvanceAnimationsUntilEnd();
|
|
|
|
// Make sure nothing is overscrolled.
|
|
EXPECT_FALSE(child->IsOverscrolled());
|
|
EXPECT_FALSE(rootApzc->IsOverscrolled());
|
|
}
|
|
#endif
|
|
|
|
#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
|
|
TEST_F(APZScrollHandoffTesterMock, StuckInOverscroll_Bug1240202b) {
|
|
// Enable overscrolling.
|
|
SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
|
|
|
|
CreateScrollHandoffLayerTree1();
|
|
|
|
TestAsyncPanZoomController* child = ApzcOf(layers[1]);
|
|
|
|
// Pan, causing the parent APZC to overscroll.
|
|
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
|
|
Pan(manager, 60, 90, PanOptions::KeepFingerDown);
|
|
EXPECT_FALSE(child->IsOverscrolled());
|
|
EXPECT_TRUE(rootApzc->IsOverscrolled());
|
|
|
|
// Lift the finger, triggering an overscroll animation
|
|
// (but don't allow it to run).
|
|
TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time());
|
|
|
|
// Put the finger down again, interrupting the animation
|
|
// and entering the TOUCHING state.
|
|
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
|
|
TouchDown(manager, ScreenIntPoint(10, 90), mcc->Time());
|
|
|
|
// Put a second finger down. Since we're in the TOUCHING state,
|
|
// the "are we panned into overscroll" check will fail and we
|
|
// will not ignore the second finger, instead entering the
|
|
// PINCHING state.
|
|
MultiTouchInput secondFingerDown(MultiTouchInput::MULTITOUCH_START, 0,
|
|
mcc->Time(), 0);
|
|
// Use the same touch identifier for the first touch (0) as TouchDown(). (A
|
|
// bit hacky.)
|
|
secondFingerDown.mTouches.AppendElement(
|
|
SingleTouchData(0, ScreenIntPoint(10, 90), ScreenSize(0, 0), 0, 0));
|
|
secondFingerDown.mTouches.AppendElement(
|
|
SingleTouchData(1, ScreenIntPoint(10, 80), ScreenSize(0, 0), 0, 0));
|
|
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
|
|
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
|
|
manager->ReceiveInputEvent(secondFingerDown);
|
|
|
|
// Release the fingers.
|
|
MultiTouchInput fingersUp = secondFingerDown;
|
|
fingersUp.mType = MultiTouchInput::MULTITOUCH_END;
|
|
manager->ReceiveInputEvent(fingersUp);
|
|
|
|
// Allow any animations to run their course.
|
|
child->AdvanceAnimationsUntilEnd();
|
|
rootApzc->AdvanceAnimationsUntilEnd();
|
|
|
|
// Make sure nothing is overscrolled.
|
|
EXPECT_FALSE(child->IsOverscrolled());
|
|
EXPECT_FALSE(rootApzc->IsOverscrolled());
|
|
}
|
|
#endif
|
|
|
|
#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android
|
|
TEST_F(APZScrollHandoffTester, OpposingConstrainedAxes_Bug1201098) {
|
|
// Enable overscrolling.
|
|
SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
|
|
|
|
CreateScrollHandoffLayerTree4();
|
|
|
|
RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
|
|
|
|
// Pan, causing the child APZC to overscroll.
|
|
Pan(childApzc, 50, 60);
|
|
|
|
// Make sure only the child is overscrolled.
|
|
EXPECT_TRUE(childApzc->IsOverscrolled());
|
|
EXPECT_FALSE(rootApzc->IsOverscrolled());
|
|
}
|
|
#endif
|
|
|
|
// Test the behaviour when flinging diagonally and there is room to scroll in
|
|
// one direction but not the other. In the direction where there is room to
|
|
// scroll, the fling should continue. In the direction where there is no room
|
|
// to scroll, the fling should stop without being handed off to the parent.
|
|
TEST_F(APZScrollHandoffTesterMock, DiagonalFlingNoHandoff) {
|
|
SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
|
|
|
|
CreateScrollHandoffLayerTree1();
|
|
|
|
// Fling up and to the left. The child APZC has room to scroll up, but not
|
|
// to the left.
|
|
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
|
|
Pan(manager, ScreenIntPoint(90, 90), ScreenIntPoint(55, 55));
|
|
|
|
RefPtr<TestAsyncPanZoomController> parent = ApzcOf(layers[0]);
|
|
RefPtr<TestAsyncPanZoomController> child = ApzcOf(layers[1]);
|
|
|
|
// Advance the child's fling animation once to give any potential handoff
|
|
// a chance to occur.
|
|
mcc->AdvanceByMillis(10);
|
|
child->AdvanceAnimations(mcc->GetSampleTime());
|
|
|
|
// Assert that the child is still flinging but the parent is not (no handoff
|
|
// occurred).
|
|
child->AssertStateIsFling();
|
|
parent->AssertStateIsReset();
|
|
}
|
|
|
|
// Here we test that if two flings are happening simultaneously, overscroll
|
|
// is handed off correctly for each.
|
|
TEST_F(APZScrollHandoffTester, SimultaneousFlings) {
|
|
SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true);
|
|
SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
|
|
|
|
// Set up an initial APZC tree.
|
|
CreateScrollHandoffLayerTree3();
|
|
|
|
RefPtr<TestAsyncPanZoomController> parent1 = ApzcOf(layers[1]);
|
|
RefPtr<TestAsyncPanZoomController> child1 = ApzcOf(layers[2]);
|
|
RefPtr<TestAsyncPanZoomController> parent2 = ApzcOf(layers[3]);
|
|
RefPtr<TestAsyncPanZoomController> child2 = ApzcOf(layers[4]);
|
|
|
|
// Pan on the lower child.
|
|
Pan(child2, 45, 5);
|
|
|
|
// Pan on the upper child.
|
|
Pan(child1, 95, 55);
|
|
|
|
// Check that child1 and child2 are in a FLING state.
|
|
child1->AssertStateIsFling();
|
|
child2->AssertStateIsFling();
|
|
|
|
// Advance the animations on child1 and child2 until their end.
|
|
child1->AdvanceAnimationsUntilEnd();
|
|
child2->AdvanceAnimationsUntilEnd();
|
|
|
|
// Check that the flings have been handed off to the parents.
|
|
child1->AssertStateIsReset();
|
|
parent1->AssertStateIsFling();
|
|
child2->AssertStateIsReset();
|
|
parent2->AssertStateIsFling();
|
|
}
|
|
|
|
TEST_F(APZScrollHandoffTester, ImmediateHandoffDisallowed_Pan) {
|
|
SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", false);
|
|
|
|
CreateScrollHandoffLayerTree1();
|
|
|
|
RefPtr<TestAsyncPanZoomController> parentApzc = ApzcOf(layers[0]);
|
|
RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
|
|
|
|
// Pan on the child, enough to scroll it to its end and have scroll
|
|
// left to hand off. Since immediate handoff is disallowed, we expect
|
|
// the leftover scroll not to be handed off.
|
|
Pan(childApzc, 60, 5);
|
|
|
|
// Verify that the parent has not scrolled.
|
|
EXPECT_EQ(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y);
|
|
EXPECT_EQ(0, parentApzc->GetFrameMetrics().GetVisualScrollOffset().y);
|
|
|
|
// Pan again on the child. This time, since the child was scrolled to
|
|
// its end when the gesture began, we expect the scroll to be handed off.
|
|
Pan(childApzc, 60, 50);
|
|
|
|
// Verify that the parent scrolled.
|
|
EXPECT_EQ(10, parentApzc->GetFrameMetrics().GetVisualScrollOffset().y);
|
|
}
|
|
|
|
TEST_F(APZScrollHandoffTester, ImmediateHandoffDisallowed_Fling) {
|
|
SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", false);
|
|
SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f);
|
|
|
|
CreateScrollHandoffLayerTree1();
|
|
|
|
RefPtr<TestAsyncPanZoomController> parentApzc = ApzcOf(layers[0]);
|
|
RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
|
|
|
|
// Pan on the child, enough to get very close to the end, so that the
|
|
// subsequent fling reaches the end and has leftover velocity to hand off.
|
|
Pan(childApzc, 60, 2);
|
|
|
|
// Allow the fling to run its course.
|
|
childApzc->AdvanceAnimationsUntilEnd();
|
|
parentApzc->AdvanceAnimationsUntilEnd();
|
|
|
|
// Verify that the parent has not scrolled.
|
|
// The first comparison needs to be an ASSERT_NEAR because the fling
|
|
// computations are such that the final scroll position can be within
|
|
// COORDINATE_EPSILON of the end rather than right at the end.
|
|
ASSERT_NEAR(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y,
|
|
COORDINATE_EPSILON);
|
|
EXPECT_EQ(0, parentApzc->GetFrameMetrics().GetVisualScrollOffset().y);
|
|
|
|
// Pan again on the child. This time, since the child was scrolled to
|
|
// its end when the gesture began, we expect the scroll to be handed off.
|
|
Pan(childApzc, 60, 40);
|
|
|
|
// Allow the fling to run its course. The fling should also be handed off.
|
|
childApzc->AdvanceAnimationsUntilEnd();
|
|
parentApzc->AdvanceAnimationsUntilEnd();
|
|
|
|
// Verify that the parent scrolled from the fling.
|
|
EXPECT_GT(parentApzc->GetFrameMetrics().GetVisualScrollOffset().y, 10);
|
|
}
|
|
|
|
TEST_F(APZScrollHandoffTester, CrossApzcAxisLock_TouchAction) {
|
|
TestCrossApzcAxisLock();
|
|
}
|
|
|
|
TEST_F(APZScrollHandoffTesterMock, WheelHandoffAfterDirectionReversal) {
|
|
// Explicitly set the wheel transaction timeout pref because the test relies
|
|
// on its value.
|
|
SCOPED_GFX_PREF_INT("mousewheel.transaction.timeout", 1500);
|
|
|
|
// Set up a basic scroll handoff layer tree.
|
|
CreateScrollHandoffLayerTree1();
|
|
|
|
rootApzc = ApzcOf(layers[0]);
|
|
RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
|
|
FrameMetrics& rootMetrics = rootApzc->GetFrameMetrics();
|
|
FrameMetrics& childMetrics = childApzc->GetFrameMetrics();
|
|
CSSRect childScrollRange = childMetrics.CalculateScrollRange();
|
|
|
|
EXPECT_EQ(0, rootMetrics.GetVisualScrollOffset().y);
|
|
EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y);
|
|
|
|
ScreenIntPoint cursorLocation(10, 60); // positioned to hit the subframe
|
|
ScreenPoint upwardDelta(0, -10);
|
|
ScreenPoint downwardDelta(0, 10);
|
|
|
|
// First wheel upwards. This will have no effect because we're already
|
|
// scrolled to the top.
|
|
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
|
|
Wheel(manager, cursorLocation, upwardDelta, mcc->Time());
|
|
EXPECT_EQ(0, rootMetrics.GetVisualScrollOffset().y);
|
|
EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y);
|
|
|
|
// Now wheel downwards 6 times. This should scroll the child, and get it
|
|
// to the bottom of its 50px scroll range.
|
|
for (size_t i = 0; i < 6; ++i) {
|
|
mcc->AdvanceByMillis(100);
|
|
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
|
|
Wheel(manager, cursorLocation, downwardDelta, mcc->Time());
|
|
}
|
|
EXPECT_EQ(0, rootMetrics.GetVisualScrollOffset().y);
|
|
EXPECT_EQ(childScrollRange.YMost(), childMetrics.GetVisualScrollOffset().y);
|
|
|
|
// Wheel downwards an additional 16 times, with 100ms increments.
|
|
// This should be enough to overcome the 1500ms wheel transaction timeout
|
|
// and start scrolling the root.
|
|
for (size_t i = 0; i < 16; ++i) {
|
|
mcc->AdvanceByMillis(100);
|
|
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
|
|
Wheel(manager, cursorLocation, downwardDelta, mcc->Time());
|
|
}
|
|
EXPECT_EQ(childScrollRange.YMost(), childMetrics.GetVisualScrollOffset().y);
|
|
EXPECT_GT(rootMetrics.GetVisualScrollOffset().y, 0);
|
|
}
|
|
|
|
TEST_F(APZScrollHandoffTesterMock, WheelHandoffNonscrollable) {
|
|
// Set up a basic scroll layer tree.
|
|
CreateScrollHandoffLayerTree5();
|
|
|
|
RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
|
|
FrameMetrics& childMetrics = childApzc->GetFrameMetrics();
|
|
|
|
EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y);
|
|
|
|
ScreenPoint downwardDelta(0, 10);
|
|
// Positioned to hit the nonscrollable parent frame
|
|
ScreenIntPoint nonscrollableLocation(40, 10);
|
|
// Positioned to hit the scrollable subframe
|
|
ScreenIntPoint scrollableLocation(40, 60);
|
|
|
|
// Start the wheel transaction on a nonscrollable parent frame.
|
|
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
|
|
Wheel(manager, nonscrollableLocation, downwardDelta, mcc->Time());
|
|
EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y);
|
|
|
|
// Mouse moves to a scrollable subframe. This should end the transaction.
|
|
mcc->AdvanceByMillis(100);
|
|
MouseInput mouseInput(MouseInput::MOUSE_MOVE,
|
|
MouseInput::ButtonType::PRIMARY_BUTTON, 0, 0,
|
|
scrollableLocation, mcc->Time(), 0);
|
|
WidgetMouseEvent mouseEvent =
|
|
mouseInput.ToWidgetEvent<WidgetMouseEvent>(nullptr);
|
|
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
|
|
((APZInputBridge*)manager.get())->ReceiveInputEvent(mouseEvent);
|
|
|
|
// Wheel downward should scroll the subframe.
|
|
mcc->AdvanceByMillis(100);
|
|
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
|
|
Wheel(manager, scrollableLocation, downwardDelta, mcc->Time());
|
|
EXPECT_GT(childMetrics.GetVisualScrollOffset().y, 0);
|
|
}
|
|
|
|
TEST_F(APZScrollHandoffTesterMock, ChildCloseToEndOfScrollRange) {
|
|
SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true);
|
|
|
|
CreateScrollHandoffLayerTree1();
|
|
|
|
RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
|
|
|
|
FrameMetrics& rootMetrics = rootApzc->GetFrameMetrics();
|
|
FrameMetrics& childMetrics = childApzc->GetFrameMetrics();
|
|
|
|
// Zoom the page in by 3x. This needs to be reflected in the zoom level
|
|
// and composition bounds of both APZCs.
|
|
rootMetrics.SetZoom(CSSToParentLayerScale(3.0));
|
|
rootMetrics.SetCompositionBounds(ParentLayerRect(0, 0, 300, 300));
|
|
childMetrics.SetZoom(CSSToParentLayerScale(3.0));
|
|
childMetrics.SetCompositionBounds(ParentLayerRect(0, 150, 300, 150));
|
|
|
|
// Scroll the child APZC very close to the end of the scroll range.
|
|
// The scroll offset is chosen such that in CSS pixels it has 0.01 pixels
|
|
// room to scroll (less than COORDINATE_EPSILON = 0.02), but in ParentLayer
|
|
// pixels it has 0.03 pixels room (greater than COORDINATE_EPSILON).
|
|
childMetrics.SetVisualScrollOffset(CSSPoint(0, 49.99));
|
|
|
|
EXPECT_FALSE(childApzc->IsOverscrolled());
|
|
|
|
CSSPoint childBefore = childApzc->GetFrameMetrics().GetVisualScrollOffset();
|
|
CSSPoint parentBefore = rootApzc->GetFrameMetrics().GetVisualScrollOffset();
|
|
|
|
// Synthesize a pan gesture that tries to scroll the child further down.
|
|
PanGesture(PanGestureInput::PANGESTURE_START, childApzc,
|
|
ScreenIntPoint(10, 20), ScreenPoint(0, 40), mcc->Time());
|
|
mcc->AdvanceByMillis(5);
|
|
childApzc->AdvanceAnimations(mcc->GetSampleTime());
|
|
|
|
PanGesture(PanGestureInput::PANGESTURE_END, childApzc, ScreenIntPoint(10, 21),
|
|
ScreenPoint(0, 0), mcc->Time());
|
|
|
|
CSSPoint childAfter = childApzc->GetFrameMetrics().GetVisualScrollOffset();
|
|
CSSPoint parentAfter = rootApzc->GetFrameMetrics().GetVisualScrollOffset();
|
|
|
|
bool childScrolled = (childBefore != childAfter);
|
|
bool parentScrolled = (parentBefore != parentAfter);
|
|
|
|
// Check that either the child or the parent scrolled.
|
|
// (With the current implementation of comparing quantities to
|
|
// COORDINATE_EPSILON in CSS units, it will be the parent, but the important
|
|
// thing is that at least one of the child or parent scroll, i.e. we're not
|
|
// stuck in a situation where no scroll offset is changing).
|
|
EXPECT_TRUE(childScrolled || parentScrolled);
|
|
}
|
|
|
|
TEST_F(APZScrollHandoffTesterMock, ScrollJump_Bug1812227) {
|
|
// Set the touch start tolerance to 10 pixels.
|
|
SCOPED_GFX_PREF_FLOAT("apz.touch_start_tolerance", 10 / manager->GetDPI());
|
|
|
|
CreateScrollHandoffLayerTree6();
|
|
RefPtr<TestAsyncPanZoomController> childApzc = ApzcOf(layers[1]);
|
|
|
|
// Throughout the test, we record the composited vertical scroll position
|
|
// of the root scroll frame after every event or animation frame.
|
|
std::vector<CSSCoord> rootYScrollPositions;
|
|
auto SampleScrollPosition = [&]() {
|
|
rootYScrollPositions.push_back(
|
|
rootApzc->GetFrameMetrics().GetVisualScrollOffset().y);
|
|
};
|
|
|
|
// Helper function to perform a light upward flick (finger moves upward
|
|
// ==> page will scroll downward).
|
|
auto DoLightUpwardFlick = [&](bool aSimulatePaint = false) {
|
|
// Don't use Pan() because it decreases the touch start tolerance
|
|
// to almost zero, and here we want to test a codepath related to
|
|
// the touch start tolerance.
|
|
|
|
mcc->AdvanceByMillis(16);
|
|
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1);
|
|
TouchDown(manager, {30, 30}, mcc->Time());
|
|
SampleScrollPosition();
|
|
|
|
// If aSimulatePaint=true, simulate a main-thread paint arriving in between
|
|
// the touch-down (when the input block is created and the cached value
|
|
// InputBlockState::mTransformToApzc is set) and the first touch-move which
|
|
// overcomes the touch-tolerance threshold and synthesizes an additional
|
|
// touch-move event at the threshold. The paint has the effect of resetting
|
|
// transform to the APZC to zero. The bug occurs if the synthesized
|
|
// touch-move event incorrectly uses the up-to-date transform to the APZC
|
|
// rather than the value cached in InputBlockState::mTrasnformToApzc.
|
|
if (aSimulatePaint) {
|
|
// For simplicity, simulate a paint with the latest metrics stored on the
|
|
// APZC. In practice, what would be painted would be from a frame or two
|
|
// ago, but for reproducing this bug it does not matter.
|
|
ModifyFrameMetrics(root, [&](ScrollMetadata&, FrameMetrics& aMetrics) {
|
|
aMetrics = rootApzc->GetFrameMetrics();
|
|
});
|
|
ModifyFrameMetrics(layers[1],
|
|
[&](ScrollMetadata&, FrameMetrics& aMetrics) {
|
|
aMetrics = childApzc->GetFrameMetrics();
|
|
});
|
|
UpdateHitTestingTree();
|
|
}
|
|
|
|
mcc->AdvanceByMillis(16);
|
|
TouchMove(manager, {30, 10}, mcc->Time());
|
|
SampleScrollPosition();
|
|
|
|
mcc->AdvanceByMillis(16);
|
|
TouchUp(manager, {30, 10}, mcc->Time());
|
|
SampleScrollPosition();
|
|
|
|
// The root APZC should be flinging.
|
|
rootApzc->AssertStateIsFling();
|
|
};
|
|
|
|
// Peform one flick.
|
|
DoLightUpwardFlick();
|
|
|
|
// Sample the resulting fling partway. Testing shows it goes well past
|
|
// y=100, so sample it until y=100.
|
|
while (SampleAnimationsOnce() && rootYScrollPositions.back() < 100) {
|
|
SampleScrollPosition();
|
|
}
|
|
|
|
// Perform a second flick, this time simulating a paint in between
|
|
// the touch-start and touch-move.
|
|
DoLightUpwardFlick(true);
|
|
|
|
// Sample the fling until its completion.
|
|
while (SampleAnimationsOnce()) {
|
|
SampleScrollPosition();
|
|
}
|
|
|
|
// Check that the vertical root scroll position is non-decreasing
|
|
// throughout the course of the test, i.e. it never jumps back up.
|
|
for (size_t i = 0; i < (rootYScrollPositions.size() - 1); ++i) {
|
|
CSSCoord before = rootYScrollPositions[i];
|
|
CSSCoord after = rootYScrollPositions[i + 1];
|
|
EXPECT_LE(before, after);
|
|
}
|
|
}
|
|
|
|
TEST_F(APZCNestedFlingScrollHandoffTester, FlingInOppositeDirection) {
|
|
RefPtr<TestAsyncPanZoomController> rootApzc = ApzcOf(root);
|
|
|
|
ParentLayerPoint startRootOffset = rootApzc->GetCurrentAsyncScrollOffset(
|
|
AsyncTransformConsumer::eForEventHandling);
|
|
ExecuteDirectionChangingPanGesture(
|
|
ScreenIntPoint{569, 710},
|
|
{-11, -2, -107, -18, -148, -57, -133, -159, -21}, {11, 2, 42, 107, 148});
|
|
// Let any animation start and run for a few frames
|
|
ExecuteWait(TimeDuration::FromMilliseconds(154));
|
|
auto vel = subframeApzc->GetVelocityVector();
|
|
|
|
ParentLayerPoint endRootOffset = rootApzc->GetCurrentAsyncScrollOffset(
|
|
AsyncTransformConsumer::eForEventHandling);
|
|
|
|
EXPECT_EQ(vel.y, 0.0);
|
|
rootApzc->AssertStateIsReset();
|
|
subframeApzc->AssertStateIsReset();
|
|
EXPECT_EQ(startRootOffset.y, endRootOffset.y);
|
|
}
|