From 0abd065b15c2e11bf39c4e4689ea776ae82f2519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 26 May 2023 09:06:00 -0700 Subject: [PATCH] Create new option in LayoutInspectingPolicy to apply parent clipping to layout metrics (#37435) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/37435 We're implementing the `IntersectionObserver` API in React Native. That API reports the bounding rectangle of the root node and the target node (which we already implement in React Native via `LayoutableShadowNode::computeRelativeLayoutMetrics`), plus the intersection rectangle. This adds a new option in `LayoutInspectingPolicy` so we can get the layout metrics when clipping is applied (e.g.: if a parent node has `overflow: hidden` and we get the layout metrics of one of its children that extends beyond the limits of the parent, it would report only the rectangle that's visible for that node). Changelog: [internal] Reviewed By: sammy-SC Differential Revision: D45866245 fbshipit-source-id: ed3bfc2021e6b7819b8efea6fa3a81ed9d0bb181 --- .../renderer/core/LayoutableShadowNode.cpp | 13 +- .../renderer/core/LayoutableShadowNode.h | 1 + .../core/tests/LayoutableShadowNodeTest.cpp | 143 ++++++++++++++++++ .../react/renderer/graphics/Rect.h | 18 +++ 4 files changed, 174 insertions(+), 1 deletion(-) 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,