diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp index 9ab11bb4174..41b31772b18 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp @@ -179,7 +179,7 @@ LayoutMetrics LayoutableShadowNode::computeRelativeLayoutMetrics( resultFrame.origin = {0, 0}; // Step 3. - // Iterating on a list of nodes computing compound offset. + // Iterating on a list of nodes computing compound offset and size. auto size = shadowNodeList.size(); for (size_t i = 0; i < size; i++) { auto currentShadowNode = @@ -205,6 +205,7 @@ LayoutMetrics LayoutableShadowNode::computeRelativeLayoutMetrics( auto isRootNode = currentShadowNode->getTraits().check( ShadowNodeTraits::Trait::RootNodeKind); + auto shouldApplyTransformation = (policy.includeTransform && !isRootNode) || (policy.includeViewportOffset && isRootNode); @@ -223,6 +224,16 @@ LayoutMetrics LayoutableShadowNode::computeRelativeLayoutMetrics( policy.includeTransform) { resultFrame.origin += currentShadowNode->getContentOriginOffset(); } + + if (policy.enableOverflowClipping) { + auto overflowInset = currentShadowNode->getLayoutMetrics().overflowInset; + auto overflowRect = insetBy( + currentFrame * currentShadowNode->getTransform(), overflowInset); + resultFrame = Rect::intersect(resultFrame, overflowRect); + if (resultFrame.size.width == 0 && resultFrame.size.height == 0) { + return EmptyLayoutMetrics; + } + } } // ------------------------------ diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h index 7bb9e0f0d9f..ed0dc700886 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h @@ -46,6 +46,7 @@ class LayoutableShadowNode : public ShadowNode { struct LayoutInspectingPolicy { bool includeTransform{true}; bool includeViewportOffset{false}; + bool enableOverflowClipping{false}; }; using UnsharedList = butter:: diff --git a/packages/react-native/ReactCommon/react/renderer/core/tests/LayoutableShadowNodeTest.cpp b/packages/react-native/ReactCommon/react/renderer/core/tests/LayoutableShadowNodeTest.cpp index 46ae7c61b1e..1dd21542502 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/tests/LayoutableShadowNodeTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/tests/LayoutableShadowNodeTest.cpp @@ -384,6 +384,149 @@ TEST(LayoutableShadowNodeTest, relativeLayoutMetricsOnTransformedParent) { EXPECT_EQ(relativeLayoutMetrics.frame.size.height, 25); } +/* + * ┌────────────────────────┐ + * │ │ + * │ ┌─────────────────────┐│ + * │ │ ││ + * │ │ ┌──────────────┐││ + * │ │ │ │││ + * │ │ │ ┌──────────┐│││ + * │ │ │ │ ││││ + * │ │ │ │ ││││ + * │ │ │ │ ││││ + * │ │ │ └──────────┘│││ + * │ │ └──────────────┘││ + * │ └─────────────────────┘│ + * └────────────────────────┘ + */ +TEST(LayoutableShadowNodeTest, relativeLayoutMetricsOnParentWithClipping) { + auto builder = simpleComponentBuilder(); + + auto childShadowNode = std::shared_ptr{}; + // clang-format off + auto element = + Element() + .finalize([](RootShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.size = {900, 900}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {10, 10}; + layoutMetrics.frame.size = {100, 100}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .reference(childShadowNode) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {10, 10}; + layoutMetrics.frame.size = {150, 150}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + }) + }); + // clang-format on + + auto parentShadowNode = builder.build(element); + + auto relativeLayoutMetrics = + LayoutableShadowNode::computeRelativeLayoutMetrics( + childShadowNode->getFamily(), + *parentShadowNode, + { + /* includeTransform = */ true, + /* includeViewportOffset = */ false, + /* enableOverflowClipping = */ true, + }); + + EXPECT_EQ(relativeLayoutMetrics.frame.origin.x, 20); + EXPECT_EQ(relativeLayoutMetrics.frame.origin.y, 20); + + EXPECT_EQ(relativeLayoutMetrics.frame.size.width, 90); + EXPECT_EQ(relativeLayoutMetrics.frame.size.height, 90); +} + +/* + * ┌────────────────────────┐ + * │ │ + * │ ┌─────────────────────┐│ + * │ │ ││ + * │ │ ┌──────────────┐││ + * │ │ │ │││ + * │ │ │ ┌──────────┐│││ + * │ │ │ │ ││││ + * │ │ │ │ ││││ + * │ │ │ │ ││││ + * │ │ │ └──────────┘│││ + * │ │ └──────────────┘││ + * │ └─────────────────────┘│ + * └────────────────────────┘ + */ +TEST( + LayoutableShadowNodeTest, + relativeLayoutMetricsOnTransformedParentWithClipping) { + auto builder = simpleComponentBuilder(); + + auto childShadowNode = std::shared_ptr{}; + // clang-format off + auto element = + Element() + .finalize([](RootShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.size = {900, 900}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .props([] { + auto sharedProps = std::make_shared(); + sharedProps->transform = Transform::Scale(0.5, 0.5, 1); + return sharedProps; + }) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {10, 10}; + layoutMetrics.frame.size = {100, 100}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .reference(childShadowNode) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {10, 10}; + layoutMetrics.frame.size = {150, 150}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + }) + }); + // clang-format on + + auto parentShadowNode = builder.build(element); + + auto relativeLayoutMetrics = + LayoutableShadowNode::computeRelativeLayoutMetrics( + childShadowNode->getFamily(), + *parentShadowNode, + { + /* includeTransform = */ true, + /* includeViewportOffset = */ false, + /* enableOverflowClipping = */ true, + }); + + EXPECT_EQ(relativeLayoutMetrics.frame.origin.x, 40); + EXPECT_EQ(relativeLayoutMetrics.frame.origin.y, 40); + + EXPECT_EQ(relativeLayoutMetrics.frame.size.width, 45); + EXPECT_EQ(relativeLayoutMetrics.frame.size.height, 45); +} + /* * ┌────────────────┐ * │ │ diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/Rect.h b/packages/react-native/ReactCommon/react/renderer/graphics/Rect.h index 6c549ac02db..321b21472c9 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/Rect.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/Rect.h @@ -69,6 +69,24 @@ struct Rect { point.y <= (origin.y + size.height); } + static Rect intersect(Rect const &rect1, Rect const &rect2) { + Float x1 = std::max(rect1.origin.x, rect2.origin.x); + Float y1 = std::max(rect1.origin.y, rect2.origin.y); + Float x2 = std::min( + rect1.origin.x + rect1.size.width, rect2.origin.x + rect2.size.width); + Float y2 = std::min( + rect1.origin.y + rect1.size.height, rect2.origin.y + rect2.size.height); + + Float intersectionWidth = x2 - x1; + Float intersectionHeight = y2 - y1; + + if (intersectionWidth < 0 || intersectionHeight < 0) { + return {}; + } + + return {{x1, y1}, {intersectionWidth, intersectionHeight}}; + } + static Rect boundingRect( Point const &a, Point const &b,