/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include #include #include #include #include #include #include namespace facebook::react { // Note: the (x, y) origin is always relative to the parent node. You may use // P482342650 to re-create this test case in playground. // *******************************************************┌─ABCD:────┐**** // *******************************************************│ {70,-50} │**** // *******************************************************│ {30,60} │**** // *******************************************************│ │**** // *******************************************************│ │**** // *******************┌─A: {0,0}{50,50}──┐****************│ │**** // *******************│ │****************│ │**** // *******************│ ┌─AB:──────┐ │****************│ │**** // *******************│ │ {10,10}{30,90}****************│ │**** // *******************│ │ ┌─ABC: {10,10}{110,20}──────┤ ├───┐ // *******************│ │ │ │ │ │ // *******************│ │ │ └──────────┘ │ // *******************│ │ └──────┬───┬───────────────────────────────┘ // *******************│ │ │ │******************************** // *******************└───┤ ├───┘******************************** // ***********************│ │************************************ // ***********************│ │************************************ // ┌─ABE: {-60,50}{70,20}─┴───┐ │************************************ // │ │ │************************************ // │ │ │************************************ // │ │ │************************************ // │ │ │************************************ // └──────────────────────┬───┘ │************************************ // ***********************│ │************************************ // ***********************└──────────┘************************************ enum TestCase { AS_IS, CLIPPING, TRANSFORM_SCALE, TRANSFORM_TRANSLATE }; class LayoutTest : public ::testing::Test { protected: ComponentBuilder builder_; std::shared_ptr rootShadowNode_; std::shared_ptr viewShadowNodeA_; std::shared_ptr viewShadowNodeAB_; std::shared_ptr viewShadowNodeABC_; std::shared_ptr viewShadowNodeABCD_; std::shared_ptr viewShadowNodeABE_; LayoutTest() : builder_(simpleComponentBuilder()) {} void initialize(TestCase testCase) { // clang-format off auto element = Element() .reference(rootShadowNode_) .tag(1) .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; props.layoutConstraints = LayoutConstraints{{0,0}, {500, 500}}; auto &yogaStyle = props.yogaStyle; yogaStyle.dimensions()[YGDimensionWidth] = YGValue{200, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionHeight] = YGValue{200, YGUnitPoint}; return sharedProps; }) .children({ Element() .reference(viewShadowNodeA_) .tag(2) .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; auto &yogaStyle = props.yogaStyle; yogaStyle.positionType() = YGPositionTypeAbsolute; yogaStyle.dimensions()[YGDimensionWidth] = YGValue{50, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionHeight] = YGValue{50, YGUnitPoint}; return sharedProps; }) .children({ Element() .reference(viewShadowNodeAB_) .tag(3) .props([=] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; auto &yogaStyle = props.yogaStyle; yogaStyle.positionType() = YGPositionTypeAbsolute; yogaStyle.position()[YGEdgeLeft] = YGValue{10, YGUnitPoint}; yogaStyle.position()[YGEdgeTop] = YGValue{10, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionWidth] = YGValue{30, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionHeight] = YGValue{90, YGUnitPoint}; if (testCase == TRANSFORM_SCALE) { props.transform = props.transform * Transform::Scale(2, 2, 1); } if (testCase == TRANSFORM_TRANSLATE) { props.transform = props.transform * Transform::Translate(10, 10, 0); } return sharedProps; }) .children({ Element() .reference(viewShadowNodeABC_) .tag(4) .props([=] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; auto &yogaStyle = props.yogaStyle; if (testCase == CLIPPING) { yogaStyle.overflow() = YGOverflowHidden; } yogaStyle.positionType() = YGPositionTypeAbsolute; yogaStyle.position()[YGEdgeLeft] = YGValue{10, YGUnitPoint}; yogaStyle.position()[YGEdgeTop] = YGValue{10, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionWidth] = YGValue{110, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionHeight] = YGValue{20, YGUnitPoint}; return sharedProps; }) .children({ Element() .reference(viewShadowNodeABCD_) .tag(5) .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; auto &yogaStyle = props.yogaStyle; yogaStyle.positionType() = YGPositionTypeAbsolute; yogaStyle.position()[YGEdgeLeft] = YGValue{70, YGUnitPoint}; yogaStyle.position()[YGEdgeTop] = YGValue{-50, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionWidth] = YGValue{30, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionHeight] = YGValue{60, YGUnitPoint}; return sharedProps; }) }), Element() .reference(viewShadowNodeABE_) .tag(6) .props([] { auto sharedProps = std::make_shared(); auto &props = *sharedProps; auto &yogaStyle = props.yogaStyle; yogaStyle.positionType() = YGPositionTypeAbsolute; yogaStyle.position()[YGEdgeLeft] = YGValue{-60, YGUnitPoint}; yogaStyle.position()[YGEdgeTop] = YGValue{50, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionWidth] = YGValue{70, YGUnitPoint}; yogaStyle.dimensions()[YGDimensionHeight] = YGValue{20, YGUnitPoint}; return sharedProps; }) }) }) }); // clang-format on builder_.build(element); rootShadowNode_->layoutIfNeeded(); } }; // Test the layout as described above with no extra changes TEST_F(LayoutTest, overflowInsetTest) { initialize(AS_IS); auto layoutMetricsA = viewShadowNodeA_->getLayoutMetrics(); EXPECT_EQ(layoutMetricsA.frame.size.width, 50); EXPECT_EQ(layoutMetricsA.frame.size.height, 50); EXPECT_EQ(layoutMetricsA.overflowInset.left, -50); EXPECT_EQ(layoutMetricsA.overflowInset.top, -30); EXPECT_EQ(layoutMetricsA.overflowInset.right, -80); EXPECT_EQ(layoutMetricsA.overflowInset.bottom, -50); auto layoutMetricsABC = viewShadowNodeABC_->getLayoutMetrics(); EXPECT_EQ(layoutMetricsABC.frame.size.width, 110); EXPECT_EQ(layoutMetricsABC.frame.size.height, 20); EXPECT_EQ(layoutMetricsABC.overflowInset.left, 0); EXPECT_EQ(layoutMetricsABC.overflowInset.top, -50); EXPECT_EQ(layoutMetricsABC.overflowInset.right, 0); EXPECT_EQ(layoutMetricsABC.overflowInset.bottom, 0); } // Test when box ABC has clipping (aka overflow hidden) TEST_F(LayoutTest, overflowInsetWithClippingTest) { initialize(CLIPPING); auto layoutMetricsA = viewShadowNodeA_->getLayoutMetrics(); EXPECT_EQ(layoutMetricsA.frame.size.width, 50); EXPECT_EQ(layoutMetricsA.frame.size.height, 50); EXPECT_EQ(layoutMetricsA.overflowInset.left, -50); EXPECT_EQ(layoutMetricsA.overflowInset.top, 0); EXPECT_EQ(layoutMetricsA.overflowInset.right, -80); EXPECT_EQ(layoutMetricsA.overflowInset.bottom, -50); auto layoutMetricsABC = viewShadowNodeABC_->getLayoutMetrics(); EXPECT_EQ(layoutMetricsABC.frame.size.width, 110); EXPECT_EQ(layoutMetricsABC.frame.size.height, 20); EXPECT_EQ(layoutMetricsABC.overflowInset.left, 0); EXPECT_EQ(layoutMetricsABC.overflowInset.top, 0); EXPECT_EQ(layoutMetricsABC.overflowInset.right, 0); EXPECT_EQ(layoutMetricsABC.overflowInset.bottom, 0); } // Test when box AB translate (10, 10, 0) in transform. The parent node's // overflowInset will be affected, but the transformed node and its child nodes // are not affected. Here is an example: // // ┌────────────────┐ ┌────────────────┐ // │Original Layout │ │ Translate AB │ // └────────────────┘ └────────────────┘ // ─────▶ // ┌ ─ ─ ─ ┬──────────┐─ ─ ─ ─ ┐ ┌ ─ ─ ─ ┬──────────┐─ ─ ─ ─ ─ ┐ // │ A │ │ A │ // │ │ │ │ │ │ │ │ // ─ ─ ─ ─│─ ─ ─┌───┐┼ ─ ─ ─ ─ │ │ // │ │ │AB ││ │ │ ┌ ─ ─ ┼ ─ ─ ─ ┬──┴┬ ─ ─ ─ ─ ┤ // └─────┤ ├┘ └───────┤AB │ // │ │┌──┴─────────┤ │ │ │ │ │ // ││ABC │ │┌──┴─────────┐ // │ │└──┬─────────┤ │ │ │ ││ABC │ // ┌───ABD───────┴─┐ │ │ │└──┬─────────┘ // ├─────────────┬─┘ │ │ │ │ ├───ABD───────┴─┐ │ │ // ─ ─ ─ ─ ─ ─ ─└───┘─ ─ ─ ─ ─ ▼ └─────────────┬─┘ │ // └ ┴ ─ ─ ─ ─ ─ ─ ┴───┴ ─ ─ ─ ─ ┘ TEST_F(LayoutTest, overflowInsetTransformTranslateTest) { initialize(TRANSFORM_TRANSLATE); auto layoutMetricsA = viewShadowNodeA_->getLayoutMetrics(); EXPECT_EQ(layoutMetricsA.frame.size.width, 50); EXPECT_EQ(layoutMetricsA.frame.size.height, 50); // Change on parent node // The top/left values are NOT changing as overflowInset is union of before // and after transform layout. In this case, we move to the right and bottom, // so the left and top is not changing, while right and bottom values are // increased. EXPECT_EQ(layoutMetricsA.overflowInset.left, -50); EXPECT_EQ(layoutMetricsA.overflowInset.top, -30); EXPECT_EQ(layoutMetricsA.overflowInset.right, -90); EXPECT_EQ(layoutMetricsA.overflowInset.bottom, -60); auto layoutMetricsAB = viewShadowNodeAB_->getLayoutMetrics(); EXPECT_EQ(layoutMetricsAB.frame.size.width, 30); EXPECT_EQ(layoutMetricsAB.frame.size.height, 90); // No change on self node with translate transform EXPECT_EQ(layoutMetricsAB.overflowInset.left, -60); EXPECT_EQ(layoutMetricsAB.overflowInset.top, -40); EXPECT_EQ(layoutMetricsAB.overflowInset.right, -90); EXPECT_EQ(layoutMetricsAB.overflowInset.bottom, 0); auto layoutMetricsABC = viewShadowNodeABC_->getLayoutMetrics(); EXPECT_EQ(layoutMetricsABC.frame.size.width, 110); EXPECT_EQ(layoutMetricsABC.frame.size.height, 20); // No change on child node EXPECT_EQ(layoutMetricsABC.overflowInset.left, 0); EXPECT_EQ(layoutMetricsABC.overflowInset.top, -50); EXPECT_EQ(layoutMetricsABC.overflowInset.right, 0); EXPECT_EQ(layoutMetricsABC.overflowInset.bottom, 0); } // Test when box AB scaled 2X in transform. The parent node's overflowInset will // be affected. However, the transformed node and its child nodes only appears // to be affected (dashed arrow). Since all transform is cosmetic only, the // actual values are NOT changed. It will be converted later when mapping the // values to pixels during rendering. Here is an example: // // ┌────────────────┐ ┌────────────────┐ // │Original Layout │ │ Scale AB │ // └────────────────┘ └────────────────┘ // ─────▶ // ┌ ─ ─ ─ ┬──────────┐─ ─ ─ ─ ┐ ┌ ─ ─ ─ ─ ─ ┬──────────┐─ ─ ─ ─ ─ ┐ // │ A │ │ A │ // │ │ │ │ ├ ─ ─ ─ ─ ─ ┼ ─ ─┌─────┤─ ─ ─ ─ ─ ┤ // ─ ─ ─ ─│─ ─ ─┌───┐┼ ─ ─ ─ ─ │ │AB │ ─ ─ ─▶ // │ │ │AB ││ │ │ │ │ │ │ // └─────┤ ├┘ └────┤ │ // │ │┌──┴─────────┤ │ │ ┌───┴──────────┤ // ││ABC │ │ │ABC │ // │ │└──┬─────────┤ │ │ │ │ │ // ┌───ABD───────┴─┐ │ │ │ └───┬──────────┘ // ├─────────────┬─┘ │ │ │ ├────────────────┴──┐ │ │ // ─ ─ ─ ─ ─ ─ ─└───┘─ ─ ─ ─ ─ ▼ │ ABD │ │ // ├────────────────┬──┘ │ │ // ─ ─ ─ ─ ─ ─ ─ ─ ┴─────┴ ─ ─ ─ ─ ─ TEST_F(LayoutTest, overflowInsetTransformScaleTest) { initialize(TRANSFORM_SCALE); auto layoutMetricsA = viewShadowNodeA_->getLayoutMetrics(); EXPECT_EQ(layoutMetricsA.frame.size.width, 50); EXPECT_EQ(layoutMetricsA.frame.size.height, 50); // Change on parent node when a child view scale up // Note that AB scale up from its center point. The numbers are calculated // assuming AB's center point is not moving. EXPECT_EQ(layoutMetricsA.overflowInset.left, -125); EXPECT_EQ(layoutMetricsA.overflowInset.top, -115); EXPECT_EQ(layoutMetricsA.overflowInset.right, -185); EXPECT_EQ(layoutMetricsA.overflowInset.bottom, -95); auto layoutMetricsAB = viewShadowNodeAB_->getLayoutMetrics(); // The frame of box AB won't actually scale up. The transform matrix is // purely cosmetic and should apply later in mounting phase. EXPECT_EQ(layoutMetricsAB.frame.size.width, 30); EXPECT_EQ(layoutMetricsAB.frame.size.height, 90); // No change on self node with scale transform. This may sound a bit // surprising, but the overflowInset values will be scaled up via pixel // density ratio along with width/height of the view. When we do hit-testing, // the overflowInset value will appears to be doubled as expected. EXPECT_EQ(layoutMetricsAB.overflowInset.left, -60); EXPECT_EQ(layoutMetricsAB.overflowInset.top, -40); EXPECT_EQ(layoutMetricsAB.overflowInset.right, -90); EXPECT_EQ(layoutMetricsAB.overflowInset.bottom, 0); auto layoutMetricsABC = viewShadowNodeABC_->getLayoutMetrics(); // The frame of box ABC won't actually scale up. The transform matrix is // purely cosmatic and should apply later in mounting phase. EXPECT_EQ(layoutMetricsABC.frame.size.width, 110); EXPECT_EQ(layoutMetricsABC.frame.size.height, 20); // The overflowInset of ABC won't change either. This may sound a bit // surprising, but the overflowInset values will be scaled up via pixel // density ratio along with width/height of the view. When we do hit-testing, // the overflowInset value will appears to be doubled as expected. EXPECT_EQ(layoutMetricsABC.overflowInset.left, 0); EXPECT_EQ(layoutMetricsABC.overflowInset.top, -50); EXPECT_EQ(layoutMetricsABC.overflowInset.right, 0); EXPECT_EQ(layoutMetricsABC.overflowInset.bottom, 0); } } // namespace facebook::react