Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#include "APZCTreeManagerTester.h"
#include "APZTestCommon.h"
#include "InputUtils.h"
#include "Units.h"
#include "mozilla/BasicEvents.h"
#include "mozilla/StaticPrefs_apz.h"
class APZCTreeManagerGenericTester : public APZCTreeManagerTester {
protected:
void CreateSimpleScrollingLayer() {
const char* treeShape = "x";
LayerIntRect layerVisibleRect[] = {
LayerIntRect(0, 0, 200, 200),
};
CreateScrollData(treeShape, layerVisibleRect);
SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID,
CSSRect(0, 0, 500, 500));
}
void CreateSimpleMultiLayerTree() {
const char* treeShape = "x(xx)";
// LayerID 0 12
LayerIntRect layerVisibleRect[] = {
LayerIntRect(0, 0, 100, 100),
LayerIntRect(0, 0, 100, 50),
LayerIntRect(0, 50, 100, 50),
};
CreateScrollData(treeShape, layerVisibleRect);
}
void CreatePotentiallyLeakingTree() {
const char* treeShape = "x(x(x(x))x(x(x)))";
// LayerID 0 1 2 3 4 5 6
CreateScrollData(treeShape);
SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID);
SetScrollableFrameMetrics(layers[2],
ScrollableLayerGuid::START_SCROLL_ID + 1);
SetScrollableFrameMetrics(layers[5],
ScrollableLayerGuid::START_SCROLL_ID + 1);
SetScrollableFrameMetrics(layers[3],
ScrollableLayerGuid::START_SCROLL_ID + 2);
SetScrollableFrameMetrics(layers[6],
ScrollableLayerGuid::START_SCROLL_ID + 3);
}
void CreateTwoLayerTree(int32_t aRootContentLayerIndex) {
const char* treeShape = "x(x)";
// LayerID 0 1
LayerIntRect layerVisibleRect[] = {
LayerIntRect(0, 0, 100, 100),
LayerIntRect(0, 0, 100, 100),
};
CreateScrollData(treeShape, layerVisibleRect);
SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID);
SetScrollableFrameMetrics(layers[1],
ScrollableLayerGuid::START_SCROLL_ID + 1);
SetScrollHandoff(layers[1], layers[0]);
// Make layers[aRootContentLayerIndex] the root content
ModifyFrameMetrics(layers[aRootContentLayerIndex],
[](ScrollMetadata& sm, FrameMetrics& fm) {
fm.SetIsRootContent(true);
});
}
};
TEST_F(APZCTreeManagerGenericTester, ScrollablePaintedLayers) {
CreateSimpleMultiLayerTree();
ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
// both layers have the same scrollId
SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID);
SetScrollableFrameMetrics(layers[2], ScrollableLayerGuid::START_SCROLL_ID);
UpdateHitTestingTree();
TestAsyncPanZoomController* nullAPZC = nullptr;
// so they should have the same APZC
EXPECT_FALSE(HasScrollableFrameMetrics(layers[0]));
EXPECT_NE(nullAPZC, ApzcOf(layers[1]));
EXPECT_NE(nullAPZC, ApzcOf(layers[2]));
EXPECT_EQ(ApzcOf(layers[1]), ApzcOf(layers[2]));
}
TEST_F(APZCTreeManagerGenericTester, Bug1068268) {
CreatePotentiallyLeakingTree();
ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
UpdateHitTestingTree();
RefPtr<HitTestingTreeNode> root = manager->GetRootNode();
RefPtr<HitTestingTreeNode> node2 = root->GetFirstChild()->GetFirstChild();
RefPtr<HitTestingTreeNode> node5 = root->GetLastChild()->GetLastChild();
EXPECT_EQ(ApzcOf(layers[2]), node5->GetApzc());
EXPECT_EQ(ApzcOf(layers[2]), node2->GetApzc());
EXPECT_EQ(ApzcOf(layers[0]), ApzcOf(layers[2])->GetParent());
EXPECT_EQ(ApzcOf(layers[2]), ApzcOf(layers[5]));
EXPECT_EQ(node2->GetFirstChild(), node2->GetLastChild());
EXPECT_EQ(ApzcOf(layers[3]), node2->GetLastChild()->GetApzc());
EXPECT_EQ(node5->GetFirstChild(), node5->GetLastChild());
EXPECT_EQ(ApzcOf(layers[6]), node5->GetLastChild()->GetApzc());
EXPECT_EQ(ApzcOf(layers[2]), ApzcOf(layers[3])->GetParent());
EXPECT_EQ(ApzcOf(layers[5]), ApzcOf(layers[6])->GetParent());
}
class APZCTreeManagerGenericTesterMock : public APZCTreeManagerGenericTester {
public:
APZCTreeManagerGenericTesterMock() { CreateMockHitTester(); }
};
TEST_F(APZCTreeManagerGenericTesterMock, Bug1194876) {
// Create a layer tree with parent and child scrollable layers, with the
// child being the root content.
CreateTwoLayerTree(1);
ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
UpdateHitTestingTree();
uint64_t blockId;
nsTArray<ScrollableLayerGuid> targets;
// First touch goes down, APZCTM will hit layers[1] because it is on top of
// layers[0], but we tell it the real target APZC is layers[0].
MultiTouchInput mti;
mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
mti.mTouches.AppendElement(
SingleTouchData(0, ScreenIntPoint(25, 50), ScreenSize(0, 0), 0, 0));
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
{CompositorHitTestFlags::eVisibleToHitTest,
CompositorHitTestFlags::eIrregularArea});
blockId = manager->ReceiveInputEvent(mti).mInputBlockId;
manager->ContentReceivedInputBlock(blockId, false);
targets.AppendElement(ApzcOf(layers[0])->GetGuid());
manager->SetTargetAPZC(blockId, targets);
// Around here, the above touch will get processed by ApzcOf(layers[0])
// Second touch goes down (first touch remains down), APZCTM will again hit
// layers[1]. Again we tell it both touches landed on layers[0], but because
// layers[1] is the RCD layer, it will end up being the multitouch target.
mti.mTouches.AppendElement(
SingleTouchData(1, ScreenIntPoint(75, 50), ScreenSize(0, 0), 0, 0));
// Each touch will get hit-tested, so queue two hit-test results.
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
{CompositorHitTestFlags::eVisibleToHitTest,
CompositorHitTestFlags::eIrregularArea});
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
{CompositorHitTestFlags::eVisibleToHitTest,
CompositorHitTestFlags::eIrregularArea});
blockId = manager->ReceiveInputEvent(mti).mInputBlockId;
manager->ContentReceivedInputBlock(blockId, false);
targets.AppendElement(ApzcOf(layers[0])->GetGuid());
manager->SetTargetAPZC(blockId, targets);
// Around here, the above multi-touch will get processed by ApzcOf(layers[1]).
// We want to ensure that ApzcOf(layers[0]) has had its state cleared, because
// otherwise it will do things like dispatch spurious long-tap events.
EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _, _)).Times(0);
}
TEST_F(APZCTreeManagerGenericTesterMock, TargetChangesMidGesture_Bug1570559) {
// Create a layer tree with parent and child scrollable layers, with the
// parent being the root content.
CreateTwoLayerTree(0);
ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
UpdateHitTestingTree();
uint64_t blockId;
nsTArray<ScrollableLayerGuid> targets;
// First touch goes down. APZCTM hits the child layer because it is on top
// (and we confirm this target), but do not prevent-default the event, causing
// the child APZC's gesture detector to start a long-tap timeout task.
MultiTouchInput mti =
CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time());
mti.mTouches.AppendElement(
SingleTouchData(0, ScreenIntPoint(25, 50), ScreenSize(0, 0), 0, 0));
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
{CompositorHitTestFlags::eVisibleToHitTest,
CompositorHitTestFlags::eIrregularArea});
blockId = manager->ReceiveInputEvent(mti).mInputBlockId;
manager->ContentReceivedInputBlock(blockId, /* default prevented = */ false);
targets.AppendElement(ApzcOf(layers[1])->GetGuid());
manager->SetTargetAPZC(blockId, targets);
// Second touch goes down (first touch remains down). APZCTM again hits the
// child and we confirm this, but multi-touch events are routed to the root
// content APZC which is the parent. This event is prevent-defaulted, so we
// clear the parent's gesture state. The bug is that we fail to clear the
// child's gesture state.
mti.mTouches.AppendElement(
SingleTouchData(1, ScreenIntPoint(75, 50), ScreenSize(0, 0), 0, 0));
// Each touch will get hit-tested, so queue two hit-test results.
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
{CompositorHitTestFlags::eVisibleToHitTest,
CompositorHitTestFlags::eIrregularArea});
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1,
{CompositorHitTestFlags::eVisibleToHitTest,
CompositorHitTestFlags::eIrregularArea});
blockId = manager->ReceiveInputEvent(mti).mInputBlockId;
manager->ContentReceivedInputBlock(blockId, /* default prevented = */ true);
targets.AppendElement(ApzcOf(layers[1])->GetGuid());
manager->SetTargetAPZC(blockId, targets);
// If we've failed to clear the child's gesture state, then the long tap
// timeout task will fire in TearDown() and a long-tap will be dispatched.
EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _, _)).Times(0);
}
TEST_F(APZCTreeManagerGenericTesterMock, Bug1198900) {
// This is just a test that cancels a wheel event to make sure it doesn't
// crash.
CreateSimpleScrollingLayer();
ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
UpdateHitTestingTree();
ScreenPoint origin(100, 50);
ScrollWheelInput swi(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT,
ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 10,
false, WheelDeltaAdjustmentStrategy::eNone);
uint64_t blockId;
QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
{CompositorHitTestFlags::eVisibleToHitTest,
CompositorHitTestFlags::eIrregularArea});
blockId = manager->ReceiveInputEvent(swi).mInputBlockId;
manager->ContentReceivedInputBlock(blockId, /* preventDefault= */ true);
}
// The next two tests check that APZ clamps the scroll offset it composites even
// if the main thread fails to do so. (The main thread will always clamp its
// scroll offset internally, but it may not send APZ the clamped version for
// scroll offset synchronization reasons.)
TEST_F(APZCTreeManagerTester, Bug1551582) {
// The simple layer tree has a scrollable rect of 500x500 and a composition
// bounds of 200x200, leading to a scroll range of (0,0,300,300).
CreateSimpleScrollingLayer();
ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
UpdateHitTestingTree();
// Simulate the main thread scrolling to the end of the scroll range.
ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
aMetrics.SetLayoutScrollOffset(CSSPoint(300, 300));
nsTArray<ScrollPositionUpdate> scrollUpdates;
scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll(
ScrollOrigin::Other, CSSPoint::ToAppUnits(CSSPoint(300, 300))));
aSm.SetScrollUpdates(scrollUpdates);
aMetrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration());
});
UpdateHitTestingTree();
// Sanity check.
RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
CSSPoint compositedScrollOffset = apzc->GetCompositedScrollOffset();
EXPECT_EQ(CSSPoint(300, 300), compositedScrollOffset);
// Simulate the main thread shrinking the scrollable rect to 400x400 (and
// thereby the scroll range to (0,0,200,200) without sending a new scroll
// offset update for the clamped scroll position (200,200).
ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
aMetrics.SetScrollableRect(CSSRect(0, 0, 400, 400));
});
UpdateHitTestingTree();
// Check that APZ has clamped the scroll offset to (200,200) for us.
compositedScrollOffset = apzc->GetCompositedScrollOffset();
EXPECT_EQ(CSSPoint(200, 200), compositedScrollOffset);
}
TEST_F(APZCTreeManagerTester, Bug1557424) {
// The simple layer tree has a scrollable rect of 500x500 and a composition
// bounds of 200x200, leading to a scroll range of (0,0,300,300).
CreateSimpleScrollingLayer();
ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
UpdateHitTestingTree();
// Simulate the main thread scrolling to the end of the scroll range.
ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
aMetrics.SetLayoutScrollOffset(CSSPoint(300, 300));
nsTArray<ScrollPositionUpdate> scrollUpdates;
scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll(
ScrollOrigin::Other, CSSPoint::ToAppUnits(CSSPoint(300, 300))));
aSm.SetScrollUpdates(scrollUpdates);
aMetrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration());
});
UpdateHitTestingTree();
// Sanity check.
RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
CSSPoint compositedScrollOffset = apzc->GetCompositedScrollOffset();
EXPECT_EQ(CSSPoint(300, 300), compositedScrollOffset);
// Simulate the main thread expanding the composition bounds to 300x300 (and
// thereby shrinking the scroll range to (0,0,200,200) without sending a new
// scroll offset update for the clamped scroll position (200,200).
ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
aMetrics.SetCompositionBounds(ParentLayerRect(0, 0, 300, 300));
});
UpdateHitTestingTree();
// Check that APZ has clamped the scroll offset to (200,200) for us.
compositedScrollOffset = apzc->GetCompositedScrollOffset();
EXPECT_EQ(CSSPoint(200, 200), compositedScrollOffset);
}
TEST_F(APZCTreeManagerTester, Bug1805601) {
// The simple layer tree has a scrollable rect of 500x500 and a composition
// bounds of 200x200, leading to a scroll range of (0,0,300,300) at unit zoom.
CreateSimpleScrollingLayer();
ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
UpdateHitTestingTree();
RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);
FrameMetrics& compositorMetrics = apzc->GetFrameMetrics();
EXPECT_EQ(CSSRect(0, 0, 300, 300), compositorMetrics.CalculateScrollRange());
// Zoom the page in by 2x. This needs to be reflected in each of the pres
// shell resolution, cumulative resolution, and zoom. This makes the scroll
// range (0,0,400,400).
compositorMetrics.SetZoom(CSSToParentLayerScale(2.0));
EXPECT_EQ(CSSRect(0, 0, 400, 400), compositorMetrics.CalculateScrollRange());
// Scroll to an area inside the 2x scroll range but outside the original one.
compositorMetrics.ClampAndSetVisualScrollOffset(CSSPoint(350, 350));
EXPECT_EQ(CSSPoint(350, 350), compositorMetrics.GetVisualScrollOffset());
// Simulate a main-thread update where the zoom is reset to 1x but the visual
// scroll offset is unmodified.
ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) {
// Changes to |compositorMetrics| are not reflected in |aMetrics|, which
// is the "layer tree" copy, so we don't need to explicitly set the zoom to
// 1.0 (it still has that as the initial value), but we do need to set
// the visual scroll offset to the same value the APZ copy has.
aMetrics.SetVisualScrollOffset(CSSPoint(350, 350));
// Needed to get APZ to accept the 1.0 zoom in |aMetrics|, otherwise
// it will act as though its zoom is newer (e.g. an async zoom that hasn't
// been repainted yet) and ignore ours.
aSm.SetResolutionUpdated(true);
});
UpdateHitTestingTree();
// Check that APZ clamped the scroll offset.
EXPECT_EQ(CSSRect(0, 0, 300, 300), compositorMetrics.CalculateScrollRange());
EXPECT_EQ(CSSPoint(300, 300), compositorMetrics.GetVisualScrollOffset());
}
TEST_F(APZCTreeManagerTester,
InstantKeyScrollBetweenTwoSamplingsWithSameTimeStamp) {
if (!StaticPrefs::apz_keyboard_enabled_AtStartup()) {
// On Android apz.keyboard.enabled is false by default and it's can't be
// changed here since it's `mirror: once`, so we just skip this test.
return;
}
// For instant scrolling, i.e. no async animation should not be involved.
SCOPED_GFX_PREF_BOOL("general.smoothScroll", false);
// Set up a keyboard shortcuts map to scroll page down.
AutoTArray<KeyboardShortcut, 1> shortcuts{KeyboardShortcut(
KeyboardInput::KEY_DOWN, 0, 0, 0, 0,
KeyboardScrollAction(
KeyboardScrollAction::KeyboardScrollActionType::eScrollPage, true))};
KeyboardMap keyboardMap(std::move(shortcuts));
manager->SetKeyboardMap(keyboardMap);
// Set up a scrollable layer.
CreateSimpleScrollingLayer();
ScopedLayerTreeRegistration registration(LayersId{0}, mcc);
UpdateHitTestingTree();
// Setup the scrollable layer is scrollable by key events.
FocusTarget focusTarget;
focusTarget.mSequenceNumber = 1;
focusTarget.mData = AsVariant<FocusTarget::ScrollTargets>(
{ScrollableLayerGuid::START_SCROLL_ID,
ScrollableLayerGuid::START_SCROLL_ID});
manager->UpdateFocusState(LayersId{0}, LayersId{0}, focusTarget);
// A vsync tick happens.
mcc->AdvanceByMillis(16);
// The first sampling happens, there's no change have happened, thus no need
// to composite.
EXPECT_FALSE(manager->AdvanceAnimations(mcc->GetSampleTime()));
// A key event causing scroll page down happens.
WidgetKeyboardEvent widgetEvent(true, eKeyDown, nullptr);
KeyboardInput input(widgetEvent);
Unused << manager->ReceiveInputEvent(input);
// Simulate WebRender compositing frames until APZ tells it the scroll offset
// has stopped changing.
// Important to trigger the bug: the first composite has the same time stamp
// as the earlier one above.
ParentLayerPoint compositedScrollOffset;
while (true) {
bool needMoreFrames = manager->AdvanceAnimations(mcc->GetSampleTime());
compositedScrollOffset = ApzcOf(root)->GetCurrentAsyncScrollOffset(
AsyncPanZoomController::eForCompositing);
if (!needMoreFrames) {
break;
}
mcc->AdvanceBy(TimeDuration::FromMilliseconds(16));
}
// Check that the effect of the keyboard scroll has been composited.
EXPECT_GT(compositedScrollOffset.y, 0);
}
TEST_F(APZCTreeManagerGenericTesterMock,
PanGestureWithCtrlModifier_Bug1917488) {
// The outer layer represents the browser chrome, and has layers id 0.
// The inner layer represents web content, and will initally get layers id 1.
const char* treeShape = "x(x)";
LayerIntRect layerVisibleRect[] = {
LayerIntRect(0, 0, 100, 100),
LayerIntRect(0, 0, 100, 100),
};
CreateScrollData(treeShape, layerVisibleRect);
// We need one of these for every LayersId the test will use!
ScopedLayerTreeRegistration registration0(LayersId{0}, mcc);
ScopedLayerTreeRegistration registration1(LayersId{1}, mcc);
ScopedLayerTreeRegistration registration2(LayersId{2}, mcc);
// In this test, we only bother to give the inner layer an APZC.
ScrollableLayerGuid::ViewID scrollId = ScrollableLayerGuid::START_SCROLL_ID;
SetScrollableFrameMetrics(layers[1], scrollId, CSSRect(0, 0, 100, 100));
// Set the referent id of the root layer to 1.
// Note that this causes the *descendants* of the root layer
// to get a layers id of 1 during the hit-testing tree update.
LayersId tab1LayersId{1};
ScrollableLayerGuid tab1Guid(tab1LayersId, 0, scrollId);
root->SetReferentId(tab1LayersId);
UpdateHitTestingTree();
// Perform a pan gesture on the content layer.
// For the START event, do not use the PanGesture() helper, because we want to
// check the mLayersId that APZ sets on the input event object. This
// determines what content process the event will gets dispatched to.
ScreenIntPoint panPoint(50, 50);
QueueMockHitResult(tab1Guid);
PanGestureInput panStart(PanGestureInput::PANGESTURE_START, mcc->Time(),
panPoint, ScreenPoint(0, 10), MODIFIER_NONE);
manager->ReceiveInputEvent(panStart);
mcc->AdvanceByMillis(5);
QueueMockHitResult(tab1Guid);
PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint,
ScreenPoint(0, 10), mcc->Time());
mcc->AdvanceByMillis(5);
QueueMockHitResult(tab1Guid);
PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint,
ScreenPoint(0, 0), mcc->Time());
// The layers id assigned to the events of this gesture should be 1.
EXPECT_EQ(panStart.mLayersId, tab1LayersId);
// Simulate a tab switch by updating the referent id of the root layer
// to a new layers id, 2.
LayersId tab2LayersId{2};
ScrollableLayerGuid tab2Guid(tab2LayersId, 0, scrollId);
root->SetReferentId(tab2LayersId);
UpdateHitTestingTree();
// Perform another pan gesture on the content layer, but this time with
// the Control modifier held down.
QueueMockHitResult(tab2Guid);
PanGestureInput zoomStart(PanGestureInput::PANGESTURE_START, mcc->Time(),
panPoint, ScreenPoint(0, 10), MODIFIER_CONTROL);
manager->ReceiveInputEvent(zoomStart);
mcc->AdvanceByMillis(5);
QueueMockHitResult(tab2Guid);
PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint,
ScreenPoint(0, 10), mcc->Time(), MODIFIER_CONTROL);
mcc->AdvanceByMillis(5);
QueueMockHitResult(tab2Guid);
PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint,
ScreenPoint(0, 0), mcc->Time(), MODIFIER_CONTROL);
// The layers id assigned to the events in the zoom gesture should be 2.
// (In the buggy scenario, it's 1, meaning the events get incorrectly
// sent to the previous tab's content process.)
EXPECT_EQ(zoomStart.mLayersId, tab2LayersId);
}