| // Copyright 2017 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 "cc/animation/scroll_timeline.h" |
| #include "cc/trees/property_tree.h" |
| #include "cc/trees/scroll_node.h" |
| #include "cc/trees/transform_node.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/gfx/geometry/scroll_offset.h" |
| |
| namespace cc { |
| |
| namespace { |
| |
| void SetScrollOffset(PropertyTrees* property_trees, |
| ElementId scroller_id, |
| gfx::ScrollOffset offset) { |
| // Update both scroll and transform trees |
| property_trees->scroll_tree.SetScrollOffset(scroller_id, offset); |
| TransformNode* transform_node = |
| property_trees->transform_tree.FindNodeFromElementId(scroller_id); |
| transform_node->scroll_offset = offset; |
| transform_node->needs_local_transform_update = true; |
| } |
| |
| void CreateScrollingElement(PropertyTrees* property_trees, |
| ElementId scroller_id, |
| gfx::Size content_size, |
| gfx::Size container_size) { |
| // Create a corresponding TransformNode for the scrolling element. |
| TransformNode transform_node; |
| transform_node.scrolls = true; |
| transform_node.source_node_id = TransformTree::kRootNodeId; |
| int transform_node_id = |
| property_trees->transform_tree.Insert(transform_node, 0); |
| property_trees->element_id_to_transform_node_index[scroller_id] = |
| transform_node_id; |
| |
| // Add the scrolling node for the scrolling and link it to the above transform |
| // node. |
| ScrollNode scroll_node; |
| scroll_node.scrollable = true; |
| scroll_node.bounds = content_size; |
| scroll_node.container_bounds = container_size; |
| scroll_node.element_id = scroller_id; |
| scroll_node.transform_id = transform_node_id; |
| int scroll_node_id = property_trees->scroll_tree.Insert(scroll_node, 0); |
| |
| property_trees->element_id_to_scroll_node_index[scroller_id] = scroll_node_id; |
| } |
| |
| // Helper method to calculate the current time, implementing only step 5 of |
| // https://wicg.github.io/scroll-animations/#current-time-algorithm |
| double CalculateCurrentTime(double current_scroll_offset, |
| double start_scroll_offset, |
| double end_scroll_offset, |
| double effective_time_range) { |
| return ((current_scroll_offset - start_scroll_offset) / |
| (end_scroll_offset - start_scroll_offset)) * |
| effective_time_range; |
| } |
| |
| } // namespace |
| |
| class ScrollTimelineTest : public ::testing::Test { |
| public: |
| ScrollTimelineTest() |
| : scroller_id_(1), container_size_(100, 100), content_size_(500, 500) { |
| // For simplicity we make the property_tree main thread; this avoids the |
| // need to deal with the synced scroll offset code. |
| property_trees_.is_main_thread = true; |
| property_trees_.is_active = false; |
| |
| // Create a single scroller that is scrolling a 500x500 contents inside a |
| // 100x100 container. |
| CreateScrollingElement(&property_trees_, scroller_id_, content_size_, |
| container_size_); |
| } |
| |
| PropertyTrees& property_trees() { return property_trees_; } |
| |
| ScrollTree& scroll_tree() { return property_trees_.scroll_tree; } |
| ElementId scroller_id() const { return scroller_id_; } |
| gfx::Size container_size() const { return container_size_; } |
| gfx::Size content_size() const { return content_size_; } |
| |
| private: |
| PropertyTrees property_trees_; |
| ElementId scroller_id_; |
| gfx::Size container_size_; |
| gfx::Size content_size_; |
| }; |
| |
| TEST_F(ScrollTimelineTest, BasicCurrentTimeCalculations) { |
| // For simplicity, we set the time range such that the current time maps |
| // directly to the scroll offset. We have a square scroller/contents, so can |
| // just compute one edge and use it for vertical/horizontal. |
| double time_range = content_size().height() - container_size().height(); |
| |
| ScrollTimeline vertical_timeline(scroller_id(), ScrollTimeline::ScrollDown, |
| base::nullopt, base::nullopt, time_range); |
| ScrollTimeline horizontal_timeline(scroller_id(), ScrollTimeline::ScrollRight, |
| base::nullopt, base::nullopt, time_range); |
| |
| // Unscrolled, both timelines should read a current time of 0. |
| SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset()); |
| EXPECT_FLOAT_EQ(0, vertical_timeline.CurrentTime(scroll_tree(), false)); |
| EXPECT_FLOAT_EQ(0, horizontal_timeline.CurrentTime(scroll_tree(), false)); |
| |
| // Now do some scrolling and make sure that the ScrollTimelines update. |
| SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(75, 50)); |
| |
| // As noted above, we have mapped the time range such that current time should |
| // just be the scroll offset. |
| EXPECT_FLOAT_EQ(50, vertical_timeline.CurrentTime(scroll_tree(), false)); |
| EXPECT_FLOAT_EQ(75, horizontal_timeline.CurrentTime(scroll_tree(), false)); |
| } |
| |
| TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForTimeRange) { |
| // Here we set a time range to 100, which gives the current time the form of |
| // 'percentage scrolled'. |
| ScrollTimeline timeline(scroller_id(), ScrollTimeline::ScrollDown, |
| base::nullopt, base::nullopt, 100); |
| |
| double halfwayY = (content_size().height() - container_size().height()) / 2.; |
| SetScrollOffset(&property_trees(), scroller_id(), |
| gfx::ScrollOffset(0, halfwayY)); |
| |
| EXPECT_FLOAT_EQ(50, timeline.CurrentTime(scroll_tree(), false)); |
| } |
| |
| // This test ensures that the ScrollTimeline's active scroller id is correct. We |
| // had a few crashes caused by assuming that the id would be available in the |
| // active tree before the activation happened; see http://crbug.com/853231 |
| TEST_F(ScrollTimelineTest, ActiveTimeIsSetOnlyAfterPromotion) { |
| PropertyTrees pending_tree; |
| PropertyTrees active_tree; |
| |
| pending_tree.is_active = false; |
| active_tree.is_active = true; |
| |
| // For simplicity we pretend the trees are main thread; this avoids the need |
| // to deal with the synced scroll offset code. |
| pending_tree.is_main_thread = true; |
| active_tree.is_main_thread = true; |
| |
| // Initially only the pending tree has the scroll node. |
| ElementId scroller_id(1); |
| CreateScrollingElement(&pending_tree, scroller_id, content_size(), |
| container_size()); |
| |
| double halfwayY = (content_size().height() - container_size().height()) / 2.; |
| SetScrollOffset(&pending_tree, scroller_id, gfx::ScrollOffset(0, halfwayY)); |
| |
| ScrollTimeline main_timeline(scroller_id, ScrollTimeline::ScrollDown, |
| base::nullopt, base::nullopt, 100); |
| |
| // Now create an impl version of the ScrollTimeline. Initilly this should only |
| // have a pending scroller id, as the active tree may not yet have the |
| // scroller in it (as in this case). |
| std::unique_ptr<ScrollTimeline> impl_timeline = |
| main_timeline.CreateImplInstance(); |
| |
| EXPECT_TRUE( |
| std::isnan(impl_timeline->CurrentTime(active_tree.scroll_tree, true))); |
| EXPECT_FLOAT_EQ(50, |
| impl_timeline->CurrentTime(pending_tree.scroll_tree, false)); |
| |
| // Now fake a tree activation; this should cause the ScrollTimeline to update |
| // its active scroller id. Note that we deliberately pass in the pending_tree |
| // and just claim it is the active tree; this avoids needing to properly |
| // implement tree swapping just for the test. |
| impl_timeline->PromoteScrollTimelinePendingToActive(); |
| EXPECT_FLOAT_EQ(50, |
| impl_timeline->CurrentTime(pending_tree.scroll_tree, true)); |
| EXPECT_FLOAT_EQ(50, |
| impl_timeline->CurrentTime(pending_tree.scroll_tree, false)); |
| } |
| |
| TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForPixelSnapping) { |
| double time_range = content_size().height() - container_size().height(); |
| ScrollTimeline timeline(scroller_id(), ScrollTimeline::ScrollDown, |
| base::nullopt, base::nullopt, time_range); |
| |
| SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 50)); |
| |
| // For simplicity emulate snapping by directly setting snap_amount of |
| // transform node. |
| TransformNode* transform_node = |
| property_trees().transform_tree.FindNodeFromElementId(scroller_id()); |
| transform_node->snap_amount = gfx::Vector2dF(0, 0.5); |
| |
| EXPECT_FLOAT_EQ(49.5, timeline.CurrentTime(scroll_tree(), false)); |
| } |
| |
| TEST_F(ScrollTimelineTest, CurrentTimeHandlesStartScrollOffset) { |
| double time_range = content_size().height() - container_size().height(); |
| const double start_scroll_offset = 20; |
| ScrollTimeline timeline(scroller_id(), ScrollTimeline::ScrollDown, |
| start_scroll_offset, base::nullopt, time_range); |
| |
| // Unscrolled, the timeline should read a current time of unresolved, since |
| // the current offset (0) will be less than the startScrollOffset. |
| SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset()); |
| EXPECT_TRUE(std::isnan(timeline.CurrentTime(scroll_tree(), false))); |
| |
| SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 19)); |
| EXPECT_TRUE(std::isnan(timeline.CurrentTime(scroll_tree(), false))); |
| SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 20)); |
| EXPECT_FLOAT_EQ(0, timeline.CurrentTime(scroll_tree(), false)); |
| SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 50)); |
| EXPECT_FLOAT_EQ( |
| CalculateCurrentTime(50, start_scroll_offset, time_range, time_range), |
| timeline.CurrentTime(scroll_tree(), false)); |
| SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 200)); |
| EXPECT_FLOAT_EQ( |
| CalculateCurrentTime(200, start_scroll_offset, time_range, time_range), |
| timeline.CurrentTime(scroll_tree(), false)); |
| } |
| |
| TEST_F(ScrollTimelineTest, CurrentTimeHandlesEndScrollOffset) { |
| double time_range = content_size().height() - container_size().height(); |
| const double end_scroll_offset = time_range - 20; |
| ScrollTimeline timeline(scroller_id(), ScrollTimeline::ScrollDown, |
| base::nullopt, end_scroll_offset, time_range); |
| |
| SetScrollOffset(&property_trees(), scroller_id(), |
| gfx::ScrollOffset(0, time_range)); |
| EXPECT_TRUE(std::isnan(timeline.CurrentTime(scroll_tree(), false))); |
| SetScrollOffset(&property_trees(), scroller_id(), |
| gfx::ScrollOffset(0, time_range - 20)); |
| EXPECT_FLOAT_EQ( |
| CalculateCurrentTime(time_range - 20, 0, end_scroll_offset, time_range), |
| timeline.CurrentTime(scroll_tree(), false)); |
| SetScrollOffset(&property_trees(), scroller_id(), |
| gfx::ScrollOffset(0, time_range - 50)); |
| EXPECT_FLOAT_EQ( |
| CalculateCurrentTime(time_range - 50, 0, end_scroll_offset, time_range), |
| timeline.CurrentTime(scroll_tree(), false)); |
| SetScrollOffset(&property_trees(), scroller_id(), |
| gfx::ScrollOffset(0, time_range - 200)); |
| EXPECT_FLOAT_EQ( |
| CalculateCurrentTime(time_range - 200, 0, end_scroll_offset, time_range), |
| timeline.CurrentTime(scroll_tree(), false)); |
| } |
| |
| TEST_F(ScrollTimelineTest, CurrentTimeHandlesCombinedStartAndEndScrollOffset) { |
| double time_range = content_size().height() - container_size().height(); |
| double start_scroll_offset = 20; |
| double end_scroll_offset = time_range - 50; |
| ScrollTimeline timeline(scroller_id(), ScrollTimeline::ScrollDown, |
| start_scroll_offset, end_scroll_offset, time_range); |
| SetScrollOffset(&property_trees(), scroller_id(), |
| gfx::ScrollOffset(0, time_range - 150)); |
| EXPECT_FLOAT_EQ(CalculateCurrentTime(time_range - 150, start_scroll_offset, |
| end_scroll_offset, time_range), |
| timeline.CurrentTime(scroll_tree(), false)); |
| } |
| |
| TEST_F(ScrollTimelineTest, CurrentTimeHandlesEqualStartAndEndScrollOffset) { |
| double time_range = content_size().height() - container_size().height(); |
| ScrollTimeline timeline(scroller_id(), ScrollTimeline::ScrollDown, 20, 20, |
| time_range); |
| SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 150)); |
| EXPECT_TRUE(std::isnan(timeline.CurrentTime(scroll_tree(), false))); |
| } |
| |
| TEST_F(ScrollTimelineTest, |
| CurrentTimeHandlesStartOffsetLargerThanEndScrollOffset) { |
| double time_range = content_size().height() - container_size().height(); |
| ScrollTimeline timeline(scroller_id(), ScrollTimeline::ScrollDown, 50, 10, |
| time_range); |
| SetScrollOffset(&property_trees(), scroller_id(), gfx::ScrollOffset(0, 150)); |
| EXPECT_TRUE(std::isnan(timeline.CurrentTime(scroll_tree(), false))); |
| } |
| |
| } // namespace cc |