diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index 3a73ee13681..989df452c3a 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -564,6 +564,7 @@ android { "react_render_animations", "react_render_core", "react_render_consistency", + "react_render_dom", "react_render_graphics", "rrc_image", "rrc_root", diff --git a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt index 5c0d6a9cd7f..1e0a86425d9 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt @@ -81,6 +81,7 @@ add_react_common_subdir(react/renderer/uimanager) add_react_common_subdir(react/renderer/core) add_react_common_subdir(react/renderer/consistency) add_react_common_subdir(react/renderer/uimanager/consistency) +add_react_common_subdir(react/renderer/dom) add_react_common_subdir(react/renderer/element) add_react_common_subdir(react/renderer/graphics) add_react_common_subdir(react/renderer/debug) diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index 05d0e7259e5..d7af3b162a3 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -286,11 +286,24 @@ Pod::Spec.new do |s| ss.dependency folly_dep_name, folly_version ss.dependency "React-rendererconsistency" + ss.dependency "React-Fabric/dom" ss.compiler_flags = folly_compiler_flags ss.source_files = "react/renderer/uimanager/*.{m,mm,cpp,h}" ss.header_dir = "react/renderer/uimanager" end + s.subspec "dom" do |ss| + ss.dependency folly_dep_name, folly_version + ss.dependency "React-Fabric/components/root" + ss.dependency "React-Fabric/components/text" + ss.dependency "React-Fabric/core" + ss.dependency "React-graphics" + ss.compiler_flags = folly_compiler_flags + ss.source_files = "react/renderer/dom/**/*.{m,mm,cpp,h}" + ss.exclude_files = "react/renderer/dom/tests" + ss.header_dir = "react/renderer/dom" + end + s.subspec "telemetry" do |ss| ss.dependency folly_dep_name, folly_version ss.compiler_flags = folly_compiler_flags diff --git a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp index 2c26e9c415d..79cf1bf09db 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp @@ -6,10 +6,11 @@ */ #include "NativeDOM.h" +#include +#include #include #include #include -#include #include #ifdef RN_DISABLE_OSS_PLUGIN_HEADER @@ -22,9 +23,15 @@ std::shared_ptr NativeDOMModuleProvider( } namespace { -facebook::react::UIManager& getUIManagerFromRuntime( - facebook::jsi::Runtime& runtime) { - return facebook::react::UIManagerBinding::getBinding(runtime)->getUIManager(); +using namespace facebook::react; + +RootShadowNode::Shared getCurrentShadowTreeRevision( + facebook::jsi::Runtime& runtime, + SurfaceId surfaceId) { + auto& uiManager = + facebook::react::UIManagerBinding::getBinding(runtime)->getUIManager(); + auto shadowTreeRevisionProvider = uiManager.getShadowTreeRevisionProvider(); + return shadowTreeRevisionProvider->getCurrentRevision(surfaceId); } facebook::react::PointerEventsProcessor& getPointerEventsProcessorFromRuntime( @@ -32,6 +39,24 @@ facebook::react::PointerEventsProcessor& getPointerEventsProcessorFromRuntime( return facebook::react::UIManagerBinding::getBinding(runtime) ->getPointerEventsProcessor(); } + +std::vector getArrayOfInstanceHandlesFromShadowNodes( + const ShadowNode::ListOfShared& nodes, + facebook::jsi::Runtime& runtime) { + // JSI doesn't support adding elements to an array after creation, + // so we need to accumulate the values in a vector and then create + // the array when we know the size. + std::vector nonNullInstanceHandles; + nonNullInstanceHandles.reserve(nodes.size()); + for (const auto& shadowNode : nodes) { + auto instanceHandle = (*shadowNode).getInstanceHandle(runtime); + if (!instanceHandle.isNull()) { + nonNullInstanceHandles.push_back(std::move(instanceHandle)); + } + } + + return nonNullInstanceHandles; +} } // namespace namespace facebook::react { @@ -43,41 +68,49 @@ std::optional NativeDOM::getParentNode( jsi::Runtime& rt, jsi::Value shadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); - auto parentShadowNode = - getUIManagerFromRuntime(rt).getNewestParentOfShadowNode(*shadowNode); - - // shadowNode is a RootShadowNode - if (!parentShadowNode) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { return std::nullopt; } - return (*parentShadowNode).getInstanceHandle(rt); + auto parentShadowNode = dom::getParentNode(currentRevision, *shadowNode); + if (parentShadowNode == nullptr) { + return std::nullopt; + } + + return parentShadowNode->getInstanceHandle(rt); } std::optional> NativeDOM::getChildNodes( jsi::Runtime& rt, jsi::Value shadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); - - auto newestCloneOfShadowNode = - getUIManagerFromRuntime(rt).getNewestCloneOfShadowNode(*shadowNode); - - // There's no version of this node in the current shadow tree - if (newestCloneOfShadowNode == nullptr) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { return std::nullopt; } - auto childShadowNodes = newestCloneOfShadowNode->getChildren(); - return getArrayOfInstanceHandlesFromShadowNodes(childShadowNodes, rt); + auto childNodes = dom::getChildNodes(currentRevision, *shadowNode); + + // There's no version of this node in the current shadow tree + if (!childNodes) { + return std::nullopt; + } + + return getArrayOfInstanceHandlesFromShadowNodes(childNodes.value(), rt); } bool NativeDOM::isConnected(jsi::Runtime& rt, jsi::Value shadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + return false; + } - auto newestCloneOfShadowNode = - getUIManagerFromRuntime(rt).getNewestCloneOfShadowNode(*shadowNode); - - return newestCloneOfShadowNode != nullptr; + return dom::isConnected(currentRevision, *shadowNode); } double NativeDOM::compareDocumentPosition( @@ -86,23 +119,27 @@ double NativeDOM::compareDocumentPosition( jsi::Value otherShadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); auto otherShadowNode = shadowNodeFromValue(rt, otherShadowNodeValue); + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (otherShadowNode == nullptr || currentRevision == nullptr) { + return 0; + } - auto documentPosition = getUIManagerFromRuntime(rt).compareDocumentPosition( - *shadowNode, *otherShadowNode); - - return documentPosition; + return dom::compareDocumentPosition( + currentRevision, *shadowNode, *otherShadowNode); } std::string NativeDOM::getTextContent( jsi::Runtime& rt, jsi::Value shadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + return ""; + } - auto textContent = - getUIManagerFromRuntime(rt).getTextContentInNewestCloneOfShadowNode( - *shadowNode); - - return textContent; + return dom::getTextContent(currentRevision, *shadowNode); } std::optionalgetSurfaceId()); + if (currentRevision == nullptr) { return std::nullopt; } - auto frame = layoutMetrics.frame; - return std::tuple{ - frame.origin.x, frame.origin.y, frame.size.width, frame.size.height}; + return dom::getBoundingClientRect( + currentRevision, *shadowNode, includeTransform); } std::optional> NativeDOM::getOffset(jsi::Runtime& rt, jsi::Value shadowNodeValue) { - auto& uiManager = getUIManagerFromRuntime(rt); auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); - - auto newestCloneOfShadowNode = - uiManager.getNewestCloneOfShadowNode(*shadowNode); - auto newestPositionedAncestorOfShadowNode = - uiManager.getNewestPositionedAncestorOfShadowNode(*shadowNode); - // The node is no longer part of an active shadow tree, or it is the - // root node - if (newestCloneOfShadowNode == nullptr || - newestPositionedAncestorOfShadowNode == nullptr) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { return std::nullopt; } - // If the node is not displayed (itself or any of its ancestors has - // "display: none"), this returns an empty layout metrics object. - auto shadowNodeLayoutMetricsRelativeToRoot = - uiManager.getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ false}); - if (shadowNodeLayoutMetricsRelativeToRoot == EmptyLayoutMetrics) { + auto offset = dom::getOffset(currentRevision, *shadowNode); + + if (!offset) { return std::nullopt; } - auto positionedAncestorLayoutMetricsRelativeToRoot = - uiManager.getRelativeLayoutMetrics( - *newestPositionedAncestorOfShadowNode, - nullptr, - {/* .includeTransform = */ false}); - if (positionedAncestorLayoutMetricsRelativeToRoot == EmptyLayoutMetrics) { - return std::nullopt; - } - - auto shadowNodeOriginRelativeToRoot = - shadowNodeLayoutMetricsRelativeToRoot.frame.origin; - auto positionedAncestorOriginRelativeToRoot = - positionedAncestorLayoutMetricsRelativeToRoot.frame.origin; - - // On the Web, offsets are computed from the inner border of the - // parent. - auto offsetTop = shadowNodeOriginRelativeToRoot.y - - positionedAncestorOriginRelativeToRoot.y - - positionedAncestorLayoutMetricsRelativeToRoot.borderWidth.top; - auto offsetLeft = shadowNodeOriginRelativeToRoot.x - - positionedAncestorOriginRelativeToRoot.x - - positionedAncestorLayoutMetricsRelativeToRoot.borderWidth.left; + auto& offsetValue = offset.value(); return std::tuple{ - (*newestPositionedAncestorOfShadowNode).getInstanceHandle(rt), - offsetTop, - offsetLeft}; + std::get<0>(offsetValue)->getInstanceHandle(rt), + std::get<1>(offsetValue), + std::get<2>(offsetValue)}; } std::optional> NativeDOM::getScrollPosition(jsi::Runtime& rt, jsi::Value shadowNodeValue) { - auto& uiManager = getUIManagerFromRuntime(rt); auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); - - auto newestCloneOfShadowNode = - uiManager.getNewestCloneOfShadowNode(*shadowNode); - // The node is no longer part of an active shadow tree, or it is the - // root node - if (newestCloneOfShadowNode == nullptr) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { return std::nullopt; } - // If the node is not displayed (itself or any of its ancestors has - // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = uiManager.getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ true}); - - if (layoutMetrics == EmptyLayoutMetrics) { - return std::nullopt; - } - - auto layoutableShadowNode = - dynamic_cast(newestCloneOfShadowNode.get()); - // This should never happen - if (layoutableShadowNode == nullptr) { - return std::nullopt; - } - - auto scrollPosition = layoutableShadowNode->getContentOriginOffset(); - - return std::tuple{ - scrollPosition.x == 0 ? 0 : -scrollPosition.x, - scrollPosition.y == 0 ? 0 : -scrollPosition.y}; + return dom::getScrollPosition(currentRevision, *shadowNode); } std::optional> NativeDOM::getScrollSize(jsi::Runtime& rt, jsi::Value shadowNodeValue) { - auto& uiManager = getUIManagerFromRuntime(rt); auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); - - auto newestCloneOfShadowNode = - uiManager.getNewestCloneOfShadowNode(*shadowNode); - // The node is no longer part of an active shadow tree, or it is the - // root node - if (newestCloneOfShadowNode == nullptr) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { return std::nullopt; } - // If the node is not displayed (itself or any of its ancestors has - // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = uiManager.getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ false}); - - if (layoutMetrics == EmptyLayoutMetrics || - layoutMetrics.displayType == DisplayType::Inline) { - return std::nullopt; - } - - auto layoutableShadowNode = dynamic_cast( - newestCloneOfShadowNode.get()); - // This should never happen - if (layoutableShadowNode == nullptr) { - return std::nullopt; - } - - Size scrollSize = getScrollableContentBounds( - layoutableShadowNode->getContentBounds(), layoutMetrics) - .size; - - return std::tuple{ - std::round(scrollSize.width), std::round(scrollSize.height)}; + return dom::getScrollSize(currentRevision, *shadowNode); } std::optional> NativeDOM::getInnerSize(jsi::Runtime& rt, jsi::Value shadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); - - // If the node is not displayed (itself or any of its ancestors has - // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = getUIManagerFromRuntime(rt).getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ false}); - - if (layoutMetrics == EmptyLayoutMetrics || - layoutMetrics.displayType == DisplayType::Inline) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { return std::nullopt; } - auto paddingFrame = layoutMetrics.getPaddingFrame(); - - return std::tuple{ - std::round(paddingFrame.size.width), - std::round(paddingFrame.size.height)}; + return dom::getInnerSize(currentRevision, *shadowNode); } std::optional> NativeDOM::getBorderSize(jsi::Runtime& rt, jsi::Value shadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); - - // If the node is not displayed (itself or any of its ancestors has - // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = getUIManagerFromRuntime(rt).getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ false}); - - if (layoutMetrics == EmptyLayoutMetrics || - layoutMetrics.displayType == DisplayType::Inline) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { return std::nullopt; } - return std::tuple{ - std::round(layoutMetrics.borderWidth.top), - std::round(layoutMetrics.borderWidth.right), - std::round(layoutMetrics.borderWidth.bottom), - std::round(layoutMetrics.borderWidth.left)}; + return dom::getBorderSize(currentRevision, *shadowNode); } std::string NativeDOM::getTagName( jsi::Runtime& rt, jsi::Value shadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); - - std::string canonicalComponentName = shadowNode->getComponentName(); - - // FIXME(T162807327): Remove Android-specific prefixes and unify - // shadow node implementations - if (canonicalComponentName == "AndroidTextInput") { - canonicalComponentName = "TextInput"; - } else if (canonicalComponentName == "AndroidSwitch") { - canonicalComponentName = "Switch"; - } - - // Prefix with RN: - canonicalComponentName.insert(0, "RN:"); - - return canonicalComponentName; + return dom::getTagName(*shadowNode); } bool NativeDOM::hasPointerCapture( diff --git a/packages/react-native/ReactCommon/react/renderer/dom/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/dom/CMakeLists.txt new file mode 100644 index 00000000000..2bc7a84189b --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/dom/CMakeLists.txt @@ -0,0 +1,26 @@ +# 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. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +add_compile_options( + -fexceptions + -frtti + -std=c++20 + -Wall + -Wpedantic + -DLOG_TAG=\"Fabric\") + +file(GLOB react_render_dom_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_render_dom STATIC ${react_render_dom_SRC}) + +target_include_directories(react_render_dom PUBLIC ${REACT_COMMON_DIR}) + +target_link_libraries(react_render_dom + react_render_core + react_render_graphics + rrc_root + rrc_text) diff --git a/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp b/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp new file mode 100644 index 00000000000..f52101b7da7 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp @@ -0,0 +1,489 @@ +/* + * 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 "DOM.h" +#include +#include +#include +#include +#include + +namespace { + +using namespace facebook::react; + +// To prevent ambiguity with built-in MacOS types. +using facebook::react::Point; +using facebook::react::Rect; +using facebook::react::Size; + +constexpr uint_fast16_t DOCUMENT_POSITION_DISCONNECTED = 1; +constexpr uint_fast16_t DOCUMENT_POSITION_PRECEDING = 2; +constexpr uint_fast16_t DOCUMENT_POSITION_FOLLOWING = 4; +constexpr uint_fast16_t DOCUMENT_POSITION_CONTAINS = 8; +constexpr uint_fast16_t DOCUMENT_POSITION_CONTAINED_BY = 16; + +ShadowNode::Shared getShadowNodeInRevision( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + // If the given shadow node is of the same family as the root shadow node, + // return the latest root shadow node + if (ShadowNode::sameFamily(*currentRevision, shadowNode)) { + return currentRevision; + } + + auto ancestors = shadowNode.getFamily().getAncestors(*currentRevision); + + if (ancestors.empty()) { + return nullptr; + } + + auto pair = ancestors.rbegin(); + return pair->first.get().getChildren().at(pair->second); +} + +ShadowNode::Shared getParentShadowNodeInRevision( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + // If the given shadow node is of the same family as the root shadow node, + // return the latest root shadow node + if (ShadowNode::sameFamily(*currentRevision, shadowNode)) { + return currentRevision; + } + + auto ancestors = shadowNode.getFamily().getAncestors(*currentRevision); + + if (ancestors.empty()) { + return nullptr; + } + + if (ancestors.size() == 1) { + // The parent is the shadow root + return currentRevision; + } + + auto parentOfParentPair = ancestors[ancestors.size() - 2]; + return parentOfParentPair.first.get().getChildren().at( + parentOfParentPair.second); +} + +ShadowNode::Shared getPositionedAncestorOfShadowNodeInRevision( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto ancestors = shadowNode.getFamily().getAncestors(*currentRevision); + + if (ancestors.empty()) { + return nullptr; + } + + for (auto it = ancestors.rbegin(); it != ancestors.rend(); it++) { + const auto layoutableAncestorShadowNode = + dynamic_cast(&(it->first.get())); + if (layoutableAncestorShadowNode == nullptr) { + return nullptr; + } + if (layoutableAncestorShadowNode->getLayoutMetrics().positionType != + PositionType::Static) { + // We have found our nearest positioned ancestor, now to get a shared + // pointer of it + it++; + if (it != ancestors.rend()) { + return it->first.get().getChildren().at(it->second); + } + // else the positioned ancestor is the root which we return outside of the + // loop + } + } + + // If there is no positioned ancestor then we just consider the root + // to be one + return currentRevision; +} + +void getTextContentInShadowNode( + const ShadowNode& shadowNode, + std::string& result) { + auto rawTextShadowNode = dynamic_cast(&shadowNode); + + if (rawTextShadowNode != nullptr) { + result.append(rawTextShadowNode->getConcreteProps().text); + } + + for (const auto& childNode : shadowNode.getChildren()) { + getTextContentInShadowNode(*childNode, result); + } +} + +LayoutMetrics getRelativeLayoutMetrics( + const ShadowNode& ancestorNode, + const ShadowNode& shadowNode, + LayoutableShadowNode::LayoutInspectingPolicy policy) { + auto layoutableAncestorShadowNode = + dynamic_cast(&ancestorNode); + + if (layoutableAncestorShadowNode == nullptr) { + return EmptyLayoutMetrics; + } + + return LayoutableShadowNode::computeRelativeLayoutMetrics( + shadowNode.getFamily(), *layoutableAncestorShadowNode, policy); +} + +Rect getScrollableContentBounds( + Rect contentBounds, + LayoutMetrics layoutMetrics) { + auto paddingFrame = layoutMetrics.getPaddingFrame(); + + auto paddingBottom = + layoutMetrics.contentInsets.bottom - layoutMetrics.borderWidth.bottom; + auto paddingLeft = + layoutMetrics.contentInsets.left - layoutMetrics.borderWidth.left; + auto paddingRight = + layoutMetrics.contentInsets.right - layoutMetrics.borderWidth.right; + + auto minY = paddingFrame.getMinY(); + auto maxY = + std::max(paddingFrame.getMaxY(), contentBounds.getMaxY() + paddingBottom); + + auto minX = layoutMetrics.layoutDirection == LayoutDirection::RightToLeft + ? std::min(paddingFrame.getMinX(), contentBounds.getMinX() - paddingLeft) + : paddingFrame.getMinX(); + auto maxX = layoutMetrics.layoutDirection == LayoutDirection::RightToLeft + ? paddingFrame.getMaxX() + : std::max( + paddingFrame.getMaxX(), contentBounds.getMaxX() + paddingRight); + + return Rect{Point{minX, minY}, Size{maxX - minX, maxY - minY}}; +} + +} // namespace + +namespace facebook::react::dom { + +ShadowNode::Shared getParentNode( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + return getParentShadowNodeInRevision(currentRevision, shadowNode); +} + +std::optional> getChildNodes( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + if (shadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + return shadowNodeInCurrentRevision->getChildren(); +} + +bool isConnected( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + return shadowNodeInCurrentRevision != nullptr; +} + +uint_fast16_t compareDocumentPosition( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode, + const ShadowNode& otherShadowNode) { + if (shadowNode.getSurfaceId() != otherShadowNode.getSurfaceId()) { + return DOCUMENT_POSITION_DISCONNECTED; + } + + // Quick check for node vs. itself + if (&shadowNode == &otherShadowNode) { + return 0; + } + + auto ancestors = shadowNode.getFamily().getAncestors(*currentRevision); + if (ancestors.empty()) { + return DOCUMENT_POSITION_DISCONNECTED; + } + + auto otherAncestors = + otherShadowNode.getFamily().getAncestors(*currentRevision); + if (ancestors.empty()) { + return DOCUMENT_POSITION_DISCONNECTED; + } + + // Consume all common ancestors + size_t i = 0; + while (i < ancestors.size() && i < otherAncestors.size() && + ancestors[i].second == otherAncestors[i].second) { + i++; + } + + if (i == ancestors.size()) { + return (DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING); + } + + if (i == otherAncestors.size()) { + return (DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING); + } + + if (ancestors[i].second > otherAncestors[i].second) { + return DOCUMENT_POSITION_PRECEDING; + } + + return DOCUMENT_POSITION_FOLLOWING; +} + +std::string getTextContent( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + std::string result; + + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + + if (shadowNodeInCurrentRevision != nullptr) { + getTextContentInShadowNode(*shadowNodeInCurrentRevision, result); + } + + return result; +} + +std::optional> +getBoundingClientRect( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode, + bool includeTransform) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + if (shadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + auto layoutMetrics = getRelativeLayoutMetrics( + *currentRevision, + shadowNode, + {.includeTransform = includeTransform, .includeViewportOffset = true}); + + if (layoutMetrics == EmptyLayoutMetrics) { + return std::nullopt; + } + + auto frame = layoutMetrics.frame; + return std::tuple{ + frame.origin.x, frame.origin.y, frame.size.width, frame.size.height}; +} + +std::optional> +getOffset( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + auto positionedAncestorOfShadowNodeInCurrentRevision = + getPositionedAncestorOfShadowNodeInRevision(currentRevision, shadowNode); + + // The node is no longer part of an active shadow tree, or it is the + // root node + if (shadowNodeInCurrentRevision == nullptr || + positionedAncestorOfShadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + // If the node is not displayed (itself or any of its ancestors has + // "display: none"), this returns an empty layout metrics object. + auto shadowNodeLayoutMetricsRelativeToRoot = getRelativeLayoutMetrics( + *currentRevision, shadowNode, {.includeTransform = false}); + if (shadowNodeLayoutMetricsRelativeToRoot == EmptyLayoutMetrics) { + return std::nullopt; + } + + auto positionedAncestorLayoutMetricsRelativeToRoot = getRelativeLayoutMetrics( + *currentRevision, + *positionedAncestorOfShadowNodeInCurrentRevision, + {.includeTransform = false}); + if (positionedAncestorLayoutMetricsRelativeToRoot == EmptyLayoutMetrics) { + return std::nullopt; + } + + auto shadowNodeOriginRelativeToRoot = + shadowNodeLayoutMetricsRelativeToRoot.frame.origin; + auto positionedAncestorOriginRelativeToRoot = + positionedAncestorLayoutMetricsRelativeToRoot.frame.origin; + + // On the Web, offsets are computed from the inner border of the + // parent. + auto offsetTop = shadowNodeOriginRelativeToRoot.y - + positionedAncestorOriginRelativeToRoot.y - + positionedAncestorLayoutMetricsRelativeToRoot.borderWidth.top; + auto offsetLeft = shadowNodeOriginRelativeToRoot.x - + positionedAncestorOriginRelativeToRoot.x - + positionedAncestorLayoutMetricsRelativeToRoot.borderWidth.left; + + return std::tuple{ + positionedAncestorOfShadowNodeInCurrentRevision, offsetTop, offsetLeft}; +} + +std::optional> +getScrollPosition( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + if (shadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + // If the node is not displayed (itself or any of its ancestors has + // "display: none"), this returns an empty layout metrics object. + auto layoutMetrics = getRelativeLayoutMetrics( + *currentRevision, + *shadowNodeInCurrentRevision, + {.includeTransform = true}); + + if (layoutMetrics == EmptyLayoutMetrics) { + return std::nullopt; + } + + auto layoutableShadowNode = dynamic_cast( + shadowNodeInCurrentRevision.get()); + // This should never happen + if (layoutableShadowNode == nullptr) { + return std::nullopt; + } + + auto scrollPosition = layoutableShadowNode->getContentOriginOffset(); + + return std::tuple{ + scrollPosition.x == 0 ? 0 : -scrollPosition.x, + scrollPosition.y == 0 ? 0 : -scrollPosition.y}; +} + +std::optional> +getScrollSize( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + if (shadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + // If the node is not displayed (itself or any of its ancestors has + // "display: none"), this returns an empty layout metrics object. + auto layoutMetrics = getRelativeLayoutMetrics( + *currentRevision, + *shadowNodeInCurrentRevision, + {.includeTransform = false}); + + if (layoutMetrics == EmptyLayoutMetrics || + layoutMetrics.displayType == DisplayType::Inline) { + return std::nullopt; + } + + auto layoutableShadowNode = dynamic_cast( + shadowNodeInCurrentRevision.get()); + // This should never happen + if (layoutableShadowNode == nullptr) { + return std::nullopt; + } + + Size scrollSize = getScrollableContentBounds( + layoutableShadowNode->getContentBounds(), layoutMetrics) + .size; + + return std::tuple{ + std::round(scrollSize.width), std::round(scrollSize.height)}; +} + +std::optional> getInnerSize( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + if (shadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + // If the node is not displayed (itself or any of its ancestors has + // "display: none"), this returns an empty layout metrics object. + auto layoutMetrics = getRelativeLayoutMetrics( + *currentRevision, + *shadowNodeInCurrentRevision, + {.includeTransform = false}); + + if (layoutMetrics == EmptyLayoutMetrics || + layoutMetrics.displayType == DisplayType::Inline) { + return std::nullopt; + } + + auto paddingFrame = layoutMetrics.getPaddingFrame(); + + return std::tuple{ + std::round(paddingFrame.size.width), + std::round(paddingFrame.size.height)}; +} + +std::optional> +getBorderSize( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + if (shadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + // If the node is not displayed (itself or any of its ancestors has + // "display: none"), this returns an empty layout metrics object. + auto layoutMetrics = getRelativeLayoutMetrics( + *currentRevision, + *shadowNodeInCurrentRevision, + {.includeTransform = false}); + + if (layoutMetrics == EmptyLayoutMetrics || + layoutMetrics.displayType == DisplayType::Inline) { + return std::nullopt; + } + + return std::tuple{ + std::round(layoutMetrics.borderWidth.top), + std::round(layoutMetrics.borderWidth.right), + std::round(layoutMetrics.borderWidth.bottom), + std::round(layoutMetrics.borderWidth.left)}; +} + +std::string getTagName(const ShadowNode& shadowNode) { + std::string canonicalComponentName = shadowNode.getComponentName(); + + // FIXME(T162807327): Remove Android-specific prefixes and unify + // shadow node implementations + if (canonicalComponentName == "AndroidTextInput") { + canonicalComponentName = "TextInput"; + } else if (canonicalComponentName == "AndroidSwitch") { + canonicalComponentName = "Switch"; + } + + // Prefix with RN: + canonicalComponentName.insert(0, "RN:"); + + return canonicalComponentName; +} + +} // namespace facebook::react::dom diff --git a/packages/react-native/ReactCommon/react/renderer/dom/DOM.h b/packages/react-native/ReactCommon/react/renderer/dom/DOM.h new file mode 100644 index 00000000000..a922c514842 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/dom/DOM.h @@ -0,0 +1,87 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react::dom { + +ShadowNode::Shared getParentNode( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::optional> getChildNodes( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +bool isConnected( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +uint_fast16_t compareDocumentPosition( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode, + const ShadowNode& otherShadowNode); + +std::string getTextContent( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::optional> +getBoundingClientRect( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode, + bool includeTransform); + +std::optional> +getOffset( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::optional> +getScrollPosition( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::optional> +getScrollSize( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::optional> getInnerSize( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::optional> +getBorderSize( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::string getTagName(const ShadowNode& shadowNode); + +} // namespace facebook::react::dom diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt index c57725e3e63..15df5f7c984 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt @@ -31,6 +31,7 @@ target_link_libraries(react_render_uimanager react_render_uimanager_consistency react_render_core react_render_debug + react_render_dom react_render_graphics react_render_leakchecker react_render_runtimescheduler diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 4f000b89246..6e909a73902 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -24,12 +24,6 @@ #include namespace { -constexpr int DOCUMENT_POSITION_DISCONNECTED = 1; -constexpr int DOCUMENT_POSITION_PRECEDING = 2; -constexpr int DOCUMENT_POSITION_FOLLOWING = 4; -constexpr int DOCUMENT_POSITION_CONTAINS = 8; -constexpr int DOCUMENT_POSITION_CONTAINED_BY = 16; - std::unique_ptr constructLeakCheckerIfNeeded( const facebook::react::RuntimeExecutor& runtimeExecutor) { #ifdef REACT_NATIVE_DEBUG @@ -295,138 +289,6 @@ ShadowNode::Shared UIManager::getNewestCloneOfShadowNode( return pair->first.get().getChildren().at(pair->second); } -ShadowNode::Shared UIManager::getNewestParentOfShadowNode( - const ShadowNode& shadowNode) const { - auto ancestorShadowNode = ShadowNode::Shared{}; - shadowTreeRegistry_.visit( - shadowNode.getSurfaceId(), [&](const ShadowTree& shadowTree) { - ancestorShadowNode = shadowTree.getCurrentRevision().rootShadowNode; - }); - - if (!ancestorShadowNode) { - return nullptr; - } - - auto ancestors = shadowNode.getFamily().getAncestors(*ancestorShadowNode); - - if (ancestors.empty()) { - return nullptr; - } - - if (ancestors.size() == 1) { - // The parent is the shadow root - return ancestorShadowNode; - } - - auto parentOfParentPair = ancestors[ancestors.size() - 2]; - return parentOfParentPair.first.get().getChildren().at( - parentOfParentPair.second); -} - -ShadowNode::Shared UIManager::getNewestPositionedAncestorOfShadowNode( - const ShadowNode& shadowNode) const { - auto rootShadowNode = ShadowNode::Shared{}; - shadowTreeRegistry_.visit( - shadowNode.getSurfaceId(), [&](const ShadowTree& shadowTree) { - rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode; - }); - - if (!rootShadowNode) { - return nullptr; - } - - auto ancestors = shadowNode.getFamily().getAncestors(*rootShadowNode); - - if (ancestors.empty()) { - return nullptr; - } - - for (auto it = ancestors.rbegin(); it != ancestors.rend(); it++) { - const auto layoutableAncestorShadowNode = - dynamic_cast(&(it->first.get())); - if (layoutableAncestorShadowNode == nullptr) { - return nullptr; - } - if (layoutableAncestorShadowNode->getLayoutMetrics().positionType != - PositionType::Static) { - // We have found our nearest positioned ancestor, now to get a shared - // pointer of it - it++; - if (it != ancestors.rend()) { - return it->first.get().getChildren().at(it->second); - } - // else the positioned ancestor is the root which we return outside of the - // loop - } - } - - // If there is no positioned ancestor then we just consider the root - // to be one - return rootShadowNode; -} - -std::string UIManager::getTextContentInNewestCloneOfShadowNode( - const ShadowNode& shadowNode) const { - auto newestCloneOfShadowNode = getNewestCloneOfShadowNode(shadowNode); - std::string result; - getTextContentInShadowNode(*newestCloneOfShadowNode, result); - return result; -} - -int UIManager::compareDocumentPosition( - const ShadowNode& shadowNode, - const ShadowNode& otherShadowNode) const { - // Quick check for node vs. itself - if (&shadowNode == &otherShadowNode) { - return 0; - } - - if (shadowNode.getSurfaceId() != otherShadowNode.getSurfaceId()) { - return DOCUMENT_POSITION_DISCONNECTED; - } - - auto ancestorShadowNode = ShadowNode::Shared{}; - shadowTreeRegistry_.visit( - shadowNode.getSurfaceId(), [&](const ShadowTree& shadowTree) { - ancestorShadowNode = shadowTree.getCurrentRevision().rootShadowNode; - }); - if (!ancestorShadowNode) { - return DOCUMENT_POSITION_DISCONNECTED; - } - - auto ancestors = shadowNode.getFamily().getAncestors(*ancestorShadowNode); - if (ancestors.empty()) { - return DOCUMENT_POSITION_DISCONNECTED; - } - - auto otherAncestors = - otherShadowNode.getFamily().getAncestors(*ancestorShadowNode); - if (ancestors.empty()) { - return DOCUMENT_POSITION_DISCONNECTED; - } - - // Consume all common ancestors - size_t i = 0; - while (i < ancestors.size() && i < otherAncestors.size() && - ancestors[i].second == otherAncestors[i].second) { - i++; - } - - if (i == ancestors.size()) { - return (DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING); - } - - if (i == otherAncestors.size()) { - return (DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING); - } - - if (ancestors[i].second > otherAncestors[i].second) { - return DOCUMENT_POSITION_PRECEDING; - } - - return DOCUMENT_POSITION_FOLLOWING; -} - ShadowTreeRevisionConsistencyManager* UIManager::getShadowTreeRevisionConsistencyManager() { return lazyShadowTreeRevisionConsistencyManager_.get(); diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h index 936b963a4ce..d32dca94b19 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -97,19 +97,6 @@ class UIManager final : public ShadowTreeDelegate { ShadowNode::Shared getNewestCloneOfShadowNode( const ShadowNode& shadowNode) const; - ShadowNode::Shared getNewestParentOfShadowNode( - const ShadowNode& shadowNode) const; - - ShadowNode::Shared getNewestPositionedAncestorOfShadowNode( - const ShadowNode& shadowNode) const; - - std::string getTextContentInNewestCloneOfShadowNode( - const ShadowNode& shadowNode) const; - - int compareDocumentPosition( - const ShadowNode& shadowNode, - const ShadowNode& otherShadowNode) const; - ShadowTreeRevisionConsistencyManager* getShadowTreeRevisionConsistencyManager(); ShadowTreeRevisionProvider* getShadowTreeRevisionProvider(); diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 94f4feab378..6019609786f 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -868,25 +869,32 @@ jsi::Value UIManagerBinding::get( size_t count) -> jsi::Value { validateArgumentCount(runtime, methodName, paramCount, count); + auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); bool includeTransform = arguments[1].getBool(); - auto layoutMetrics = uiManager->getRelativeLayoutMetrics( - *shadowNodeFromValue(runtime, arguments[0]), - nullptr, - {/* .includeTransform = */ includeTransform, - /* .includeViewportOffset = */ true}); + auto currentRevision = + uiManager->getShadowTreeRevisionProvider()->getCurrentRevision( + shadowNode->getSurfaceId()); - if (layoutMetrics == EmptyLayoutMetrics) { + if (currentRevision == nullptr) { return jsi::Value::undefined(); } - auto frame = layoutMetrics.frame; + auto result = dom::getBoundingClientRect( + currentRevision, *shadowNode, includeTransform); + + if (!result) { + return jsi::Value::undefined(); + } + + auto [x, y, width, height] = result.value(); + return jsi::Array::createWithElements( runtime, - jsi::Value{runtime, (double)frame.origin.x}, - jsi::Value{runtime, (double)frame.origin.y}, - jsi::Value{runtime, (double)frame.size.width}, - jsi::Value{runtime, (double)frame.size.height}); + jsi::Value{runtime, x}, + jsi::Value{runtime, y}, + jsi::Value{runtime, width}, + jsi::Value{runtime, height}); }); } @@ -909,10 +917,18 @@ jsi::Value UIManagerBinding::get( auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); auto otherShadowNode = shadowNodeFromValue(runtime, arguments[1]); - auto documentPosition = - uiManager->compareDocumentPosition(*shadowNode, *otherShadowNode); + auto currentRevision = + uiManager->getShadowTreeRevisionProvider()->getCurrentRevision( + shadowNode->getSurfaceId()); - return jsi::Value(documentPosition); + double documentPosition = 0; + + if (currentRevision != nullptr) { + documentPosition = (double)dom::compareDocumentPosition( + currentRevision, *shadowNode, *otherShadowNode); + } + + return {documentPosition}; }); } diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h b/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h index 7984b902bc2..03552d2b140 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h @@ -11,10 +11,7 @@ #include #include #include -#include -#include #include -#include namespace facebook::react { @@ -166,70 +163,4 @@ inline static folly::dynamic commandArgsFromValue( const jsi::Value& value) { return jsi::dynamicFromValue(runtime, value); } - -inline static std::vector getArrayOfInstanceHandlesFromShadowNodes( - const ShadowNode::ListOfShared& nodes, - jsi::Runtime& runtime) { - // JSI doesn't support adding elements to an array after creation, - // so we need to accumulate the values in a vector and then create - // the array when we know the size. - std::vector nonNullInstanceHandles; - nonNullInstanceHandles.reserve(nodes.size()); - for (const auto& shadowNode : nodes) { - auto instanceHandle = (*shadowNode).getInstanceHandle(runtime); - if (!instanceHandle.isNull()) { - nonNullInstanceHandles.push_back(std::move(instanceHandle)); - } - } - - return nonNullInstanceHandles; -} - -inline static void getTextContentInShadowNode( - const ShadowNode& shadowNode, - std::string& result) { - auto rawTextShadowNode = dynamic_cast(&shadowNode); - if (rawTextShadowNode != nullptr) { - result.append(rawTextShadowNode->getConcreteProps().text); - } - - for (const auto& childNode : shadowNode.getChildren()) { - getTextContentInShadowNode(*childNode.get(), result); - } -} - -inline static Rect getScrollableContentBounds( - Rect contentBounds, - LayoutMetrics layoutMetrics) { - auto paddingFrame = layoutMetrics.getPaddingFrame(); - - auto paddingBottom = - layoutMetrics.contentInsets.bottom - layoutMetrics.borderWidth.bottom; - auto paddingLeft = - layoutMetrics.contentInsets.left - layoutMetrics.borderWidth.left; - auto paddingRight = - layoutMetrics.contentInsets.right - layoutMetrics.borderWidth.right; - - auto minY = paddingFrame.getMinY(); - auto maxY = - std::max(paddingFrame.getMaxY(), contentBounds.getMaxY() + paddingBottom); - - auto minX = layoutMetrics.layoutDirection == LayoutDirection::RightToLeft - ? std::min(paddingFrame.getMinX(), contentBounds.getMinX() - paddingLeft) - : paddingFrame.getMinX(); - auto maxX = layoutMetrics.layoutDirection == LayoutDirection::RightToLeft - ? paddingFrame.getMaxX() - : std::max( - paddingFrame.getMaxX(), contentBounds.getMaxX() + paddingRight); - - return Rect{Point{minX, minY}, Size{maxX - minX, maxY - minY}}; -} - -inline static Size getScrollSize( - LayoutMetrics layoutMetrics, - Rect contentBounds) { - auto scrollableContentBounds = - getScrollableContentBounds(contentBounds, layoutMetrics); - return scrollableContentBounds.size; -} } // namespace facebook::react