diff --git a/ReactCommon/fabric/element/BUCK b/ReactCommon/fabric/element/BUCK new file mode 100644 index 00000000000000..16ad2e7d790b04 --- /dev/null +++ b/ReactCommon/fabric/element/BUCK @@ -0,0 +1,81 @@ +load("@fbsource//tools/build_defs/apple:flag_defs.bzl", "get_debug_preprocessor_flags") +load( + "//tools/build_defs/oss:rn_defs.bzl", + "ANDROID", + "APPLE", + "CXX", + "fb_xplat_cxx_test", + "get_apple_compiler_flags", + "get_apple_inspector_flags", + "react_native_xplat_target", + "rn_xplat_cxx_library", + "subdir_glob", +) + +APPLE_COMPILER_FLAGS = get_apple_compiler_flags() + +rn_xplat_cxx_library( + name = "element", + srcs = glob( + ["**/*.cpp"], + exclude = glob(["tests/**/*.cpp"]), + ), + headers = glob( + ["**/*.h"], + exclude = glob(["tests/**/*.h"]), + ), + header_namespace = "", + exported_headers = subdir_glob( + [ + ("", "*.h"), + ], + prefix = "react/element", + ), + compiler_flags = [ + "-fexceptions", + "-frtti", + "-std=c++14", + "-Wall", + ], + fbobjc_compiler_flags = APPLE_COMPILER_FLAGS, + fbobjc_labels = ["supermodule:ios/isolation/infra.react_native"], + fbobjc_preprocessor_flags = get_debug_preprocessor_flags() + get_apple_inspector_flags(), + force_static = True, + macosx_tests_override = [], + platforms = (ANDROID, APPLE, CXX), + preprocessor_flags = [ + "-DLOG_TAG=\"ReactNative\"", + # Systraces are temporary disabled. + # "-DWITH_FBSYSTRACE=1", + ], + tests = [":tests"], + visibility = ["PUBLIC"], + deps = [ + "fbsource//xplat/fbsystrace:fbsystrace", + react_native_xplat_target("fabric/components/view:view"), + react_native_xplat_target("fabric/uimanager:uimanager"), + react_native_xplat_target("fabric/core:core"), + react_native_xplat_target("fabric/debug:debug"), + react_native_xplat_target("utils:utils"), + ], +) + +fb_xplat_cxx_test( + name = "tests", + srcs = glob(["tests/**/*.cpp"]), + headers = glob(["tests/**/*.h"]), + compiler_flags = [ + "-fexceptions", + "-frtti", + "-std=c++14", + "-Wall", + ], + contacts = ["oncall+react_native@xmail.facebook.com"], + platforms = (ANDROID, APPLE, CXX), + deps = [ + "fbsource//xplat/third-party/gmock:gtest", + ":element", + react_native_xplat_target("fabric/components/root:root"), + react_native_xplat_target("fabric/components/view:view"), + ], +) diff --git a/ReactCommon/fabric/element/ComponentBuilder.cpp b/ReactCommon/fabric/element/ComponentBuilder.cpp new file mode 100644 index 00000000000000..6424b1b6fc6a6c --- /dev/null +++ b/ReactCommon/fabric/element/ComponentBuilder.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ComponentBuilder.h" + +namespace facebook { +namespace react { + +ComponentBuilder::ComponentBuilder( + ComponentDescriptorRegistry::Shared const &componentDescriptorRegistry) + : componentDescriptorRegistry_(componentDescriptorRegistry){}; + +ShadowNode::Shared ComponentBuilder::build( + ElementFragment const &elementFragment) const { + auto &componentDescriptor = + componentDescriptorRegistry_->at(elementFragment.componentHandle); + + auto children = ShadowNode::ListOfShared{}; + children.reserve(elementFragment.children.size()); + for (auto const &childFragment : elementFragment.children) { + children.push_back(build(childFragment)); + } + + auto eventEmitter = + componentDescriptor.createEventEmitter(nullptr, elementFragment.tag); + + auto shadowNode = componentDescriptor.createShadowNode( + ShadowNodeFragment{ + elementFragment.props, + std::make_shared(children), + elementFragment.state}, + ShadowNodeFamilyFragment{ + elementFragment.tag, elementFragment.surfaceId, eventEmitter}); + + if (elementFragment.referenceCallback) { + elementFragment.referenceCallback(shadowNode); + } + + if (elementFragment.finalizeCallback) { + elementFragment.finalizeCallback(const_cast(*shadowNode)); + } + + return shadowNode; +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/element/ComponentBuilder.h b/ReactCommon/fabric/element/ComponentBuilder.h new file mode 100644 index 00000000000000..aa9b9d13b5a50d --- /dev/null +++ b/ReactCommon/fabric/element/ComponentBuilder.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) Facebook, Inc. and its 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 +#include + +namespace facebook { +namespace react { + +/* + * Build `ShadowNode` trees with a given given `Element` trees. + */ +class ComponentBuilder final { + public: + ComponentBuilder( + ComponentDescriptorRegistry::Shared const &componentDescriptorRegistry); + + /* + * Builds a `ShadowNode` tree with given `Element` tree using stored + * `ComponentDescriptorRegistry`. + */ + template + std::shared_ptr build(Element element) const { + return std::static_pointer_cast( + build(element.fragment_)); + } + + private: + /* + * Internal, type-erased version of `build`. + */ + ShadowNode::Shared build(ElementFragment const &elementFragment) const; + + ComponentDescriptorRegistry::Shared componentDescriptorRegistry_; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/element/Element.cpp b/ReactCommon/fabric/element/Element.cpp new file mode 100644 index 00000000000000..cf72b623347d17 --- /dev/null +++ b/ReactCommon/fabric/element/Element.cpp @@ -0,0 +1,10 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "Element.h" + +// Intentionally empty. diff --git a/ReactCommon/fabric/element/Element.h b/ReactCommon/fabric/element/Element.h new file mode 100644 index 00000000000000..6c25b87d96cd5c --- /dev/null +++ b/ReactCommon/fabric/element/Element.h @@ -0,0 +1,136 @@ +/* + * Copyright (c) Facebook, Inc. and its 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 + +namespace facebook { +namespace react { + +/* + * `Element<>` is an abstraction layer that allows describing component + * hierarchy in a declarative way. Creating `Element`s themself does not create + * a component tree (aka `ShadowNode` tree) but describes some hierarchical + * structure that might be used to build an actual component tree (similar to + * XML Elements). + * `Element` provides some basic type-safety guarantees: all modifications + * of element objects require using objects (such as Props or State) of + * compatible type. + */ +template +class Element final { + public: + using ConcreteProps = typename ShadowNodeT::ConcreteProps; + using SharedConcreteProps = typename ShadowNodeT::SharedConcreteProps; + using ConcreteEventEmitter = typename ShadowNodeT::ConcreteEventEmitter; + using ConcreteShadowNode = ShadowNodeT; + using ConcreteSharedShadowNode = std::shared_ptr; + + using ConcreteReferenceCallback = + std::function const &shadowNode)>; + + /* + * Constructs an `Element`. + */ + Element() { + fragment_.componentHandle = ShadowNodeT::Handle(); + fragment_.componentName = ShadowNodeT::Name(); + fragment_.props = ShadowNodeT::defaultSharedProps(); + } + + /* + * Sets `tag`. + */ + Element &tag(Tag tag) { + fragment_.tag = tag; + return *this; + } + + /* + * Sets `surfaceId`. + */ + Element &surfaceId(SurfaceId surfaceId) { + fragment_.surfaceId = surfaceId; + return *this; + } + + /* + * Sets `props`. + */ + Element &props(SharedConcreteProps props) { + fragment_.props = props; + return *this; + } + + /* + * Sets `props` using callback. + */ + Element &props(std::function callback) { + fragment_.props = callback(); + return *this; + } + + /* + * Sets children. + */ + Element &children(std::vector children) { + auto fragments = ElementFragment::List{}; + fragments.reserve(children.size()); + for (auto const &child : children) { + fragments.push_back(child.fragment_); + } + fragment_.children = fragments; + return *this; + } + + /* + * Calls the callback during component construction with a pointer to the + * component which is being constructed. + */ + Element &reference( + std::function + callback) { + fragment_.referenceCallback = callback; + return *this; + } + + /* + * During component construction, assigns a given pointer to a component + * that is being constructed. + */ + Element &reference(ConcreteSharedShadowNode &inShadowNode) { + fragment_.referenceCallback = [&](ShadowNode::Shared const &shadowNode) { + inShadowNode = + std::static_pointer_cast(shadowNode); + }; + return *this; + } + + /* + * Calls the callback with a reference to a just constructed component. + */ + Element &finalize( + std::function finalizeCallback) { + fragment_.finalizeCallback = [=](ShadowNode &shadowNode) { + return finalizeCallback(static_cast(shadowNode)); + }; + return *this; + } + + private: + friend class ComponentBuilder; + ElementFragment fragment_; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/element/ElementFragment.cpp b/ReactCommon/fabric/element/ElementFragment.cpp new file mode 100644 index 00000000000000..4421f09d3065ed --- /dev/null +++ b/ReactCommon/fabric/element/ElementFragment.cpp @@ -0,0 +1,10 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ElementFragment.h" + +// Intentionally empty. diff --git a/ReactCommon/fabric/element/ElementFragment.h b/ReactCommon/fabric/element/ElementFragment.h new file mode 100644 index 00000000000000..f5f5c9acb60bdf --- /dev/null +++ b/ReactCommon/fabric/element/ElementFragment.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) Facebook, Inc. and its 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 + +namespace facebook { +namespace react { + +/* + * This is an implementation detail, do not use it directly. + * A type-erased version of `Element<>`. + * `ElementFragment` carries all information that is stored inside `Element<>` + * in some generalized, type-erased manner. + */ +class ElementFragment final { + public: + using Shared = std::shared_ptr; + using List = std::vector; + using ListOfShared = std::vector; + using ReferenceCallback = + std::function; + using FinalizeCallback = std::function; + + /* + * ComponentDescriptor part (describes the type) + */ + ComponentHandle componentHandle; + ComponentName componentName; + + /* + * ShadowNodeFamily part (describes the family) + */ + Tag tag; + SurfaceId surfaceId; + + /* + * ShadowNode part (describes the instance) + */ + Props::Shared props; + State::Shared state; + List children; + + /* + * Other + */ + ReferenceCallback referenceCallback; + FinalizeCallback finalizeCallback; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/element/tests/ElementTest.cpp b/ReactCommon/fabric/element/tests/ElementTest.cpp new file mode 100644 index 00000000000000..43f5d487c2a281 --- /dev/null +++ b/ReactCommon/fabric/element/tests/ElementTest.cpp @@ -0,0 +1,110 @@ +/* + * Copyright (c) Facebook, Inc. and its 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 + +using namespace facebook::react; + +TEST(ElementTest, testNormalCases) { + ComponentDescriptorProviderRegistry componentDescriptorProviderRegistry{}; + auto eventDispatcher = EventDispatcher::Shared{}; + auto componentDescriptorRegistry = + componentDescriptorProviderRegistry.createComponentDescriptorRegistry( + ComponentDescriptorParameters{eventDispatcher, nullptr, nullptr}); + + componentDescriptorProviderRegistry.add( + concreteComponentDescriptorProvider()); + + auto builder = ComponentBuilder{componentDescriptorRegistry}; + + auto shadowNodeA = std::shared_ptr{}; + auto shadowNodeAA = std::shared_ptr{}; + auto shadowNodeAB = std::shared_ptr{}; + auto shadowNodeABA = std::shared_ptr{}; + + auto propsAA = std::make_shared(); + const_cast(propsAA->nativeId) = "node AA"; + + // clang-format off + auto element = + Element() + .reference(shadowNodeA) + .tag(1) + .props([]() { + auto props = std::make_shared(); + const_cast(props->zIndex) = 42; + const_cast(props->nativeId) = "node A"; + return props; + }) + .finalize([](ViewShadowNode &shadowNode){ + shadowNode.sealRecursive(); + }) + .children({ + Element() + .reference(shadowNodeAA) + .tag(2) + .props(propsAA), + Element() + .reference(shadowNodeAB) + .tag(3) + .props([]() { + auto props = std::make_shared(); + const_cast(props->nativeId) = "node AB"; + return props; + }) + .children({ + Element() + .reference(shadowNodeABA) + .tag(4) + .props([]() { + auto props = std::make_shared(); + const_cast(props->nativeId) = "node ABA"; + return props; + }) + }) + }); + // clang-format on + + auto shadowNode = builder.build(element); + + EXPECT_EQ(shadowNode, shadowNodeA); + + // Tags + EXPECT_EQ(shadowNodeA->getTag(), 1); + EXPECT_EQ(shadowNodeAA->getTag(), 2); + EXPECT_EQ(shadowNodeAB->getTag(), 3); + EXPECT_EQ(shadowNodeABA->getTag(), 4); + + // Children + EXPECT_EQ(shadowNodeA->getChildren().size(), 2); + EXPECT_EQ(shadowNodeAA->getChildren().size(), 0); + EXPECT_EQ(shadowNodeAB->getChildren().size(), 1); + EXPECT_EQ(shadowNodeABA->getChildren().size(), 0); + EXPECT_EQ( + shadowNodeA->getChildren(), + (ShadowNode::ListOfShared{shadowNodeAA, shadowNodeAB})); + EXPECT_EQ( + shadowNodeAB->getChildren(), (ShadowNode::ListOfShared{shadowNodeABA})); + + // Props + EXPECT_EQ(shadowNodeA->getProps()->nativeId, "node A"); + EXPECT_EQ(shadowNodeABA->getProps()->nativeId, "node ABA"); + EXPECT_EQ(shadowNodeAA->getProps(), propsAA); + + // Finalize + EXPECT_TRUE(shadowNodeA->getSealed()); + EXPECT_TRUE(shadowNodeAA->getSealed()); + EXPECT_TRUE(shadowNodeAB->getSealed()); + EXPECT_TRUE(shadowNodeABA->getSealed()); +}