blob: 6d66c54cf6be362b17f099146fb3eb28259401b2 [file] [log] [blame]
// 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 <limits>
#include <vector>
#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/point_f.h"
namespace cc {
namespace {
// Only expect precision up to 1 microsecond for double conversion error (mainly
// due to timeline time getting converted between double and TimeTick).
static constexpr double time_error_ms = 0.001;
#define EXPECT_SCROLL_TIMELINE_TIME_NEAR(expected, value) \
EXPECT_NEAR(expected, ToDouble(value), time_error_ms)
void SetScrollOffset(PropertyTrees* property_trees,
ElementId scroller_id,
gfx::PointF offset) {
// Update both scroll and transform trees
property_trees->scroll_tree_mutable().SetScrollOffset(scroller_id, offset);
TransformNode* transform_node =
property_trees->transform_tree_mutable().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;
int transform_node_id =
property_trees->transform_tree_mutable().Insert(transform_node, 0);
property_trees->transform_tree_mutable().SetElementIdForNodeId(
transform_node_id, scroller_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_mutable().Insert(scroll_node, 0);
property_trees->scroll_tree_mutable().SetElementIdForNodeId(scroll_node_id,
scroller_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) {
return ((current_scroll_offset - start_scroll_offset) /
(end_scroll_offset - start_scroll_offset)) *
ScrollTimeline::kScrollTimelineDurationMs;
}
// Helper method to convert base::TimeTicks to double.
// Returns double milliseconds if the input value is resolved or
// std::numeric_limits<double>::quiet_NaN() otherwise.
double ToDouble(absl::optional<base::TimeTicks> time_ticks) {
if (time_ticks)
return (time_ticks.value() - base::TimeTicks()).InMillisecondsF();
return std::numeric_limits<double>::quiet_NaN();
}
} // namespace
class ScrollTimelineTest : public ::testing::Test,
public ProtectedSequenceSynchronizer {
public:
ScrollTimelineTest()
: property_trees_(*this),
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_.set_is_main_thread(true);
property_trees_.set_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_mutable(); }
ElementId scroller_id() const { return scroller_id_; }
gfx::Size container_size() const { return container_size_; }
gfx::Size content_size() const { return content_size_; }
// ProtectedSequenceSynchronizer implementation
bool IsOwnerThread() const override { return true; }
bool InProtectedSequence() const override { return false; }
void WaitForProtectedSequenceCompletion() const override {}
private:
PropertyTrees property_trees_;
ElementId scroller_id_;
gfx::Size container_size_;
gfx::Size content_size_;
};
TEST_F(ScrollTimelineTest, BasicCurrentTimeCalculations) {
std::vector<double> scroll_offsets;
scroll_offsets.push_back(0);
scroll_offsets.push_back(100);
scoped_refptr<ScrollTimeline> vertical_timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets);
scoped_refptr<ScrollTimeline> horizontal_timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollRight, scroll_offsets);
// Unscrolled, both timelines should read a current time of 0.
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF());
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
0, vertical_timeline->CurrentTime(scroll_tree(), false));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
0, horizontal_timeline->CurrentTime(scroll_tree(), false));
// Now do some scrolling and make sure that the ScrollTimelines update.
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(75, 50));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
0.5 * ScrollTimeline::kScrollTimelineDurationMs,
vertical_timeline->CurrentTime(scroll_tree(), false));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
0.75 * ScrollTimeline::kScrollTimelineDurationMs,
horizontal_timeline->CurrentTime(scroll_tree(), false));
}
TEST_F(ScrollTimelineTest, MultipleScrollOffsetsCurrentTimeCalculations) {
double scroll_size =
content_size().height() - container_size().height(); // 400
std::vector<double> scroll_offsets;
scroll_offsets.push_back(0);
scroll_offsets.push_back(100.0);
scroll_offsets.push_back(250.0);
scroll_offsets.push_back(scroll_size);
scoped_refptr<ScrollTimeline> vertical_timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets);
unsigned int offset = 0;
double w = 1.0 / 3.0; // offset weight
double p = 0; // progress within the offset
// Scale necessary to convert absolute unit times to progress based values
double scale = ScrollTimeline::kScrollTimelineDurationMs / scroll_size;
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF());
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
(offset + p) * w * scroll_size * scale,
vertical_timeline->CurrentTime(scroll_tree(), false));
p = (70.0 - 0.0) / (100.0 - 0.0);
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 70));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
(offset + p) * w * scroll_size * scale,
vertical_timeline->CurrentTime(scroll_tree(), false));
offset = 1;
p = 0;
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 100));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
(offset + p) * w * scroll_size * scale,
vertical_timeline->CurrentTime(scroll_tree(), false));
p = (150.0 - 100.0) / (250.0 - 100.0);
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 150));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
(offset + p) * w * scroll_size * scale,
vertical_timeline->CurrentTime(scroll_tree(), false));
offset = 2;
p = 0;
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 250));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
(offset + p) * w * scroll_size * scale,
vertical_timeline->CurrentTime(scroll_tree(), false));
p = (350.0 - 250.0) / (400.0 - 250.0);
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 350));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
(offset + p) * w * scroll_size * scale,
vertical_timeline->CurrentTime(scroll_tree(), false));
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 400));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
ScrollTimeline::kScrollTimelineDurationMs,
vertical_timeline->CurrentTime(scroll_tree(), false));
}
TEST_F(ScrollTimelineTest, OverlappingScrollOffsets) {
double scroll_size = 100.0;
// Start offset is greater than end offset ==> animation progress is
// either 0% or 100%.
std::vector<double> scroll_offsets = {350.0, 200.0, 50.0};
scoped_refptr<ScrollTimeline> vertical_timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets);
// Offset is less than start offset ==> current time is 0.
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 300));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
0, vertical_timeline->CurrentTime(scroll_tree(), false));
// Scale necessary to convert absolute unit times to progress based values
double scale = ScrollTimeline::kScrollTimelineDurationMs / scroll_size;
// Offset is greater than end offset ==> current time is 100%.
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 360));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
scroll_size * scale,
vertical_timeline->CurrentTime(scroll_tree(), false));
scroll_offsets = {0.0, 400.0, 200.0};
vertical_timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets);
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 100));
// Scroll offset is 25% of [0, 400) range, which maps to [0% 50%) of the
// entire scroll range.
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
scroll_size * 0.5 * 0.25 * scale,
vertical_timeline->CurrentTime(scroll_tree(), false));
scroll_offsets = {200.0, 0.0, 400.0};
vertical_timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets);
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 300));
// Scroll offset is 75% of [0, 400) range, which maps to [50% 100%) of the
// entire scroll range.
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
scroll_size * (0.5 + 0.5 * 0.75) * scale,
vertical_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(*this);
PropertyTrees active_tree(*this);
pending_tree.set_is_active(false);
active_tree.set_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.set_is_main_thread(true);
active_tree.set_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 scroll_size = content_size().height() - container_size().height();
std::vector<double> scroll_offsets;
scroll_offsets.push_back(0);
scroll_offsets.push_back(scroll_size);
double halfwayY = scroll_size / 2.;
double expectedTime = 0.5 * ScrollTimeline::kScrollTimelineDurationMs;
SetScrollOffset(&pending_tree, scroller_id, gfx::PointF(0, halfwayY));
scoped_refptr<ScrollTimeline> main_timeline = ScrollTimeline::Create(
scroller_id, ScrollTimeline::ScrollDown, scroll_offsets);
// Now create an impl version of the ScrollTimeline. Initially this should
// only have a pending scroller id, as the active tree may not yet have the
// scroller in it (as in this case).
scoped_refptr<ScrollTimeline> impl_timeline = base::WrapRefCounted(
ToScrollTimeline(main_timeline->CreateImplInstance().get()));
EXPECT_TRUE(std::isnan(
ToDouble(impl_timeline->CurrentTime(active_tree.scroll_tree(), true))));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
expectedTime,
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->ActivateTimeline();
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
expectedTime,
impl_timeline->CurrentTime(pending_tree.scroll_tree(), true));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
expectedTime,
impl_timeline->CurrentTime(pending_tree.scroll_tree(), false));
}
TEST_F(ScrollTimelineTest, CurrentTimeIsAdjustedForPixelSnapping) {
double scroll_size = content_size().height() - container_size().height();
std::vector<double> scroll_offsets;
scroll_offsets.push_back(0);
scroll_offsets.push_back(scroll_size);
scoped_refptr<ScrollTimeline> timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets);
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 50));
// For simplicity emulate snapping by directly setting snap_amount of
// transform node.
TransformNode* transform_node =
property_trees().transform_tree_mutable().FindNodeFromElementId(
scroller_id());
transform_node->snap_amount = gfx::Vector2dF(0, 0.5);
// Scale necessary to convert absolute unit times to progress based values
double scale = ScrollTimeline::kScrollTimelineDurationMs / scroll_size;
EXPECT_SCROLL_TIMELINE_TIME_NEAR(49.5 * scale,
timeline->CurrentTime(scroll_tree(), false));
}
TEST_F(ScrollTimelineTest, CurrentTimeHandlesStartScrollOffset) {
double scroll_size = content_size().height() - container_size().height();
const double start_scroll_offset = 20;
std::vector<double> scroll_offsets;
scroll_offsets.push_back(start_scroll_offset);
scroll_offsets.push_back(scroll_size);
scoped_refptr<ScrollTimeline> timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets);
// Unscrolled, the timeline should read a current time of 0 since the current
// offset (0) will be less than the startScrollOffset.
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF());
EXPECT_SCROLL_TIMELINE_TIME_NEAR(0,
timeline->CurrentTime(scroll_tree(), false));
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 19));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(0,
timeline->CurrentTime(scroll_tree(), false));
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 20));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(0,
timeline->CurrentTime(scroll_tree(), false));
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 50));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
CalculateCurrentTime(50, start_scroll_offset, scroll_size),
timeline->CurrentTime(scroll_tree(), false));
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 200));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
CalculateCurrentTime(200, start_scroll_offset, scroll_size),
timeline->CurrentTime(scroll_tree(), false));
}
TEST_F(ScrollTimelineTest, CurrentTimeHandlesEndScrollOffset) {
double scroll_size = content_size().height() - container_size().height();
const double end_scroll_offset = scroll_size - 20;
std::vector<double> scroll_offsets;
scroll_offsets.push_back(0); // should be absl::nullopt
scroll_offsets.push_back(end_scroll_offset);
scoped_refptr<ScrollTimeline> timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets);
SetScrollOffset(&property_trees(), scroller_id(),
gfx::PointF(0, scroll_size));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(ScrollTimeline::kScrollTimelineDurationMs,
timeline->CurrentTime(scroll_tree(), false));
SetScrollOffset(&property_trees(), scroller_id(),
gfx::PointF(0, scroll_size - 20));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(ScrollTimeline::kScrollTimelineDurationMs,
timeline->CurrentTime(scroll_tree(), false));
SetScrollOffset(&property_trees(), scroller_id(),
gfx::PointF(0, scroll_size - 50));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
CalculateCurrentTime(scroll_size - 50, 0, end_scroll_offset),
timeline->CurrentTime(scroll_tree(), false));
SetScrollOffset(&property_trees(), scroller_id(),
gfx::PointF(0, scroll_size - 200));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
CalculateCurrentTime(scroll_size - 200, 0, end_scroll_offset),
timeline->CurrentTime(scroll_tree(), false));
}
TEST_F(ScrollTimelineTest, CurrentTimeHandlesCombinedStartAndEndScrollOffset) {
double scroll_size = content_size().height() - container_size().height();
double start_scroll_offset = 20;
double end_scroll_offset = scroll_size - 50;
std::vector<double> scroll_offsets;
scroll_offsets.push_back(start_scroll_offset);
scroll_offsets.push_back(end_scroll_offset);
scoped_refptr<ScrollTimeline> timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets);
SetScrollOffset(&property_trees(), scroller_id(),
gfx::PointF(0, scroll_size - 150));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(
CalculateCurrentTime(scroll_size - 150, start_scroll_offset,
end_scroll_offset),
timeline->CurrentTime(scroll_tree(), false));
}
TEST_F(ScrollTimelineTest, CurrentTimeHandlesEqualStartAndEndScrollOffset) {
std::vector<double> scroll_offsets;
scroll_offsets.push_back(20);
scroll_offsets.push_back(20);
scoped_refptr<ScrollTimeline> timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets);
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 150));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(ScrollTimeline::kScrollTimelineDurationMs,
timeline->CurrentTime(scroll_tree(), false));
}
TEST_F(ScrollTimelineTest,
CurrentTimeHandlesStartOffsetLargerThanEndScrollOffset) {
std::vector<double> scroll_offsets;
scroll_offsets.push_back(50);
scroll_offsets.push_back(10);
scoped_refptr<ScrollTimeline> timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets);
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 40));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(0,
timeline->CurrentTime(scroll_tree(), false));
SetScrollOffset(&property_trees(), scroller_id(), gfx::PointF(0, 150));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(ScrollTimeline::kScrollTimelineDurationMs,
timeline->CurrentTime(scroll_tree(), false));
}
TEST_F(ScrollTimelineTest, CurrentTimeHandlesScrollOffsets) {
const double start_scroll_offset = 20;
const double scroller_height =
content_size().height() - container_size().height();
const double end_scroll_offset = scroller_height - 20;
std::vector<double> scroll_offsets;
scroll_offsets.push_back(start_scroll_offset);
scroll_offsets.push_back(end_scroll_offset);
scoped_refptr<ScrollTimeline> timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets);
// Before the start_scroll_offset the current time should be 0
SetScrollOffset(&property_trees(), scroller_id(),
gfx::PointF(0, start_scroll_offset - 10));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(0,
timeline->CurrentTime(scroll_tree(), false));
// At the end_scroll_offset the current time should be 100%
SetScrollOffset(&property_trees(), scroller_id(),
gfx::PointF(0, end_scroll_offset));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(ScrollTimeline::kScrollTimelineDurationMs,
timeline->CurrentTime(scroll_tree(), false));
// After the end_scroll_offset the current time should be 100%
SetScrollOffset(&property_trees(), scroller_id(),
gfx::PointF(0, end_scroll_offset + 10));
EXPECT_SCROLL_TIMELINE_TIME_NEAR(ScrollTimeline::kScrollTimelineDurationMs,
timeline->CurrentTime(scroll_tree(), false));
}
TEST_F(ScrollTimelineTest, Activeness) {
// ScrollTimeline with zero scroller id is inactive.
std::vector<double> scroll_offsets;
double scroll_size = content_size().height() - container_size().height();
scroll_offsets.push_back(0);
scroll_offsets.push_back(scroll_size);
scoped_refptr<ScrollTimeline> inactive_timeline1 = ScrollTimeline::Create(
absl::nullopt, ScrollTimeline::ScrollDown, scroll_offsets);
EXPECT_FALSE(
inactive_timeline1->IsActive(scroll_tree(), false /*is_active_tree*/));
EXPECT_FALSE(
inactive_timeline1->IsActive(scroll_tree(), true /*is_active_tree*/));
// ScrollTimeline with a scroller that is not in the scroll tree is
// inactive.
scoped_refptr<ScrollTimeline> inactive_timeline2 = ScrollTimeline::Create(
ElementId(2), ScrollTimeline::ScrollDown, scroll_offsets);
EXPECT_FALSE(
inactive_timeline2->IsActive(scroll_tree(), false /*is_active_tree*/));
// Activate the scroll tree.
inactive_timeline2->ActivateTimeline();
EXPECT_FALSE(
inactive_timeline2->IsActive(scroll_tree(), true /*is_active_tree*/));
// ScrollTimeline with empty scroll offsets is inactive.
std::vector<double> empty_scroll_offsets;
scoped_refptr<ScrollTimeline> inactive_timeline3 = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, empty_scroll_offsets);
EXPECT_FALSE(
inactive_timeline3->IsActive(scroll_tree(), false /*is_active_tree*/));
EXPECT_FALSE(
inactive_timeline3->IsActive(scroll_tree(), true /*is_active_tree*/));
// Activate the scroll tree.
inactive_timeline3->ActivateTimeline();
EXPECT_FALSE(
inactive_timeline3->IsActive(scroll_tree(), false /*is_active_tree*/));
EXPECT_FALSE(
inactive_timeline3->IsActive(scroll_tree(), true /*is_active_tree*/));
scoped_refptr<ScrollTimeline> active_timeline = ScrollTimeline::Create(
scroller_id(), ScrollTimeline::ScrollDown, scroll_offsets);
EXPECT_TRUE(
active_timeline->IsActive(scroll_tree(), false /*is_active_tree*/));
EXPECT_FALSE(
active_timeline->IsActive(scroll_tree(), true /*is_active_tree*/));
// Activate the scroll tree.
active_timeline->ActivateTimeline();
EXPECT_TRUE(
active_timeline->IsActive(scroll_tree(), false /*is_active_tree*/));
EXPECT_TRUE(
active_timeline->IsActive(scroll_tree(), true /*is_active_tree*/));
}
} // namespace cc