feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,39 @@
# 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_uimanager_SRC CONFIGURE_DEPENDS *.cpp)
add_library(react_render_uimanager STATIC ${react_render_uimanager_SRC})
target_include_directories(react_render_uimanager PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(react_render_uimanager
glog
folly_runtime
jsi
react_debug
react_render_componentregistry
react_render_core
react_render_debug
react_render_graphics
react_render_leakchecker
react_render_runtimescheduler
react_render_mounting
react_config
reactnative
rrc_root
rrc_view
runtimeexecutor
)

View File

@@ -0,0 +1,30 @@
/*
* 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
namespace facebook::react {
class LayoutAnimationStatusDelegate {
public:
/**
* Called when the LayoutAnimation engine state changes from animation nothing
* to animating something. This will only be called when you go from 0 to N>0
* active animations, N to N+1 animations will not result in this being
* called.
*/
virtual void onAnimationStarted() = 0;
/**
* Called when the LayoutAnimation engine completes all pending animations.
*/
virtual void onAllAnimationsComplete() = 0;
virtual ~LayoutAnimationStatusDelegate() = default;
};
} // namespace facebook::react

View File

@@ -0,0 +1,538 @@
/*
* 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 "PointerEventsProcessor.h"
#include <glog/logging.h>
namespace facebook::react {
ShadowNode::Shared PointerEventsProcessor::getShadowNodeFromEventTarget(
jsi::Runtime& runtime,
const EventTarget* target) {
if (target != nullptr) {
target->retain(runtime);
auto instanceHandle = target->getInstanceHandle(runtime);
target->release(runtime);
if (instanceHandle.isObject()) {
auto handleObj = instanceHandle.asObject(runtime);
if (handleObj.hasProperty(runtime, "stateNode")) {
auto stateNode = handleObj.getProperty(runtime, "stateNode");
if (stateNode.isObject()) {
auto stateNodeObj = stateNode.asObject(runtime);
if (stateNodeObj.hasProperty(runtime, "node")) {
auto node = stateNodeObj.getProperty(runtime, "node");
return shadowNodeFromValue(runtime, node);
}
}
}
}
}
return nullptr;
}
static bool isViewListeningToEvents(
const ShadowNode& shadowNode,
std::initializer_list<ViewEvents::Offset> eventTypes) {
if (shadowNode.getTraits().check(ShadowNodeTraits::Trait::ViewKind)) {
auto& viewProps = static_cast<const ViewProps&>(*shadowNode.getProps());
for (const ViewEvents::Offset eventType : eventTypes) {
if (viewProps.events[eventType]) {
return true;
}
}
}
return false;
}
static bool isAnyViewInPathToRootListeningToEvents(
const UIManager& uiManager,
const ShadowNode& shadowNode,
std::initializer_list<ViewEvents::Offset> eventTypes) {
// Check the target view first
if (isViewListeningToEvents(shadowNode, eventTypes)) {
return true;
}
// Retrieve the node's root & a list of nodes between the target and the root
auto owningRootShadowNode = ShadowNode::Shared{};
uiManager.getShadowTreeRegistry().visit(
shadowNode.getSurfaceId(),
[&owningRootShadowNode](const ShadowTree& shadowTree) {
owningRootShadowNode = shadowTree.getCurrentRevision().rootShadowNode;
});
if (owningRootShadowNode == nullptr) {
return false;
}
auto& nodeFamily = shadowNode.getFamily();
auto ancestors = nodeFamily.getAncestors(*owningRootShadowNode);
// Check for listeners from the target's parent to the root
for (auto it = ancestors.rbegin(); it != ancestors.rend(); it++) {
auto& currentNode = it->first.get();
if (isViewListeningToEvents(currentNode, eventTypes)) {
return true;
}
}
return false;
}
static PointerEventTarget retargetPointerEvent(
const PointerEvent& event,
const ShadowNode& nodeToTarget,
const UIManager& uiManager) {
PointerEvent retargetedEvent(event);
// TODO: is dereferencing latestNodeToTarget without null checking safe?
auto latestNodeToTarget = uiManager.getNewestCloneOfShadowNode(nodeToTarget);
// Adjust offsetX/Y to be relative to the retargeted node
// HACK: This is a basic/incomplete implementation which simply subtracts
// the retargeted node's origin from the original event's client coordinates.
// More work will be needed to properly take non-trival transforms into
// account.
auto layoutMetrics = uiManager.getRelativeLayoutMetrics(
*latestNodeToTarget, nullptr, {/* .includeTransform */ true});
retargetedEvent.offsetPoint = {
event.clientPoint.x - layoutMetrics.frame.origin.x,
event.clientPoint.y - layoutMetrics.frame.origin.y,
};
PointerEventTarget result = {};
result.event = retargetedEvent;
result.target = latestNodeToTarget;
return result;
}
static ShadowNode::Shared getCaptureTargetOverride(
PointerIdentifier pointerId,
CaptureTargetOverrideRegistry& registry) {
auto pendingPointerItr = registry.find(pointerId);
if (pendingPointerItr == registry.end()) {
return nullptr;
}
ShadowNode::Weak maybeTarget = pendingPointerItr->second;
if (maybeTarget.expired()) {
// target has expired so it should functionally behave the same as if it
// was removed from the override list.
registry.erase(pointerId);
return nullptr;
}
return maybeTarget.lock();
}
/*
* Centralized method which determines if an event should be sent to JS by
* inspecing the listeners in the target's view path.
*/
static bool shouldEmitPointerEvent(
const ShadowNode& targetNode,
const std::string& type,
const UIManager& uiManager) {
if (type == "topPointerDown") {
return isAnyViewInPathToRootListeningToEvents(
uiManager,
targetNode,
{ViewEvents::Offset::PointerDown,
ViewEvents::Offset::PointerDownCapture});
} else if (type == "topPointerUp") {
return isAnyViewInPathToRootListeningToEvents(
uiManager,
targetNode,
{ViewEvents::Offset::PointerUp, ViewEvents::Offset::PointerUpCapture});
} else if (type == "topPointerMove") {
return isAnyViewInPathToRootListeningToEvents(
uiManager,
targetNode,
{ViewEvents::Offset::PointerMove,
ViewEvents::Offset::PointerMoveCapture});
} else if (type == "topPointerEnter") {
// This event goes through the capturing phase in full but only bubble
// through the target and no futher up the tree
return isViewListeningToEvents(
targetNode, {ViewEvents::Offset::PointerEnter}) ||
isAnyViewInPathToRootListeningToEvents(
uiManager,
targetNode,
{ViewEvents::Offset::PointerEnterCapture});
} else if (type == "topPointerLeave") {
// This event goes through the capturing phase in full but only bubble
// through the target and no futher up the tree
return isViewListeningToEvents(
targetNode, {ViewEvents::Offset::PointerLeave}) ||
isAnyViewInPathToRootListeningToEvents(
uiManager,
targetNode,
{ViewEvents::Offset::PointerLeaveCapture});
} else if (type == "topPointerOver") {
return isAnyViewInPathToRootListeningToEvents(
uiManager,
targetNode,
{ViewEvents::Offset::PointerOver,
ViewEvents::Offset::PointerOverCapture});
} else if (type == "topPointerOut") {
return isAnyViewInPathToRootListeningToEvents(
uiManager,
targetNode,
{ViewEvents::Offset::PointerOut,
ViewEvents::Offset::PointerOutCapture});
} else if (type == "topClick") {
return isAnyViewInPathToRootListeningToEvents(
uiManager,
targetNode,
{ViewEvents::Offset::Click, ViewEvents::Offset::ClickCapture});
}
// This is more of an optimization method so if we encounter a type which
// has not been specifically addressed above we should just let it through.
return true;
}
void PointerEventsProcessor::interceptPointerEvent(
const ShadowNode::Shared& target,
const std::string& type,
ReactEventPriority priority,
const PointerEvent& event,
const DispatchEvent& eventDispatcher,
const UIManager& uiManager) {
// Process all pending pointer capture assignments
processPendingPointerCapture(event, eventDispatcher, uiManager);
PointerEvent pointerEvent(event);
ShadowNode::Shared targetNode = target;
// Retarget the event if it has a pointer capture override target
auto overrideTarget = getCaptureTargetOverride(
pointerEvent.pointerId, pendingPointerCaptureTargetOverrides_);
if (overrideTarget != nullptr &&
overrideTarget->getTag() != targetNode->getTag()) {
auto retargeted =
retargetPointerEvent(pointerEvent, *overrideTarget, uiManager);
pointerEvent = retargeted.event;
targetNode = retargeted.target;
}
if (type == "topClick") {
// Click events are synthetic so should just be passed on instead of going
// through any sort of processing.
eventDispatcher(*targetNode, type, priority, pointerEvent);
return;
}
if (type == "topPointerDown") {
registerActivePointer(pointerEvent);
} else if (type == "topPointerMove") {
// TODO: Remove the need for this check by properly handling
// pointerenter/pointerleave events emitted from the native platform
if (getActivePointer(pointerEvent.pointerId) != nullptr) {
updateActivePointer(pointerEvent);
}
}
// Getting a pointerleave event from the platform is a special case telling us
// that the pointer has left the root so we don't forward the event raw but
// instead just run through our hover tracking logic with a null target.
//
// Notably: we do not forward the platform's leave event but instead will emit
// leave events through our unified hover tracking logic.
if (type == "topPointerLeave") {
handleIncomingPointerEventOnNode(
pointerEvent, nullptr, eventDispatcher, uiManager);
} else {
handleIncomingPointerEventOnNode(
pointerEvent, targetNode, eventDispatcher, uiManager);
if (shouldEmitPointerEvent(*targetNode, type, uiManager)) {
eventDispatcher(*targetNode, type, priority, pointerEvent);
}
// All pointercancel events and certain pointerup events (when using an
// direct pointer w/o the concept of hover) should be treated as the
// pointer leaving the device entirely so we go through our hover tracking
// logic again but pass in a null target.
auto activePointer = getActivePointer(pointerEvent.pointerId);
if (type == "topPointerCancel" ||
(type == "topPointerUp" && activePointer != nullptr &&
activePointer->shouldLeaveWhenReleased)) {
handleIncomingPointerEventOnNode(
pointerEvent, nullptr, eventDispatcher, uiManager);
}
}
// Implicit pointer capture release
if (overrideTarget != nullptr &&
(type == "topPointerUp" || type == "topPointerCancel")) {
releasePointerCapture(pointerEvent.pointerId, overrideTarget.get());
processPendingPointerCapture(pointerEvent, eventDispatcher, uiManager);
}
if (type == "topPointerUp" || type == "topPointerCancel") {
unregisterActivePointer(pointerEvent);
}
}
void PointerEventsProcessor::setPointerCapture(
PointerIdentifier pointerId,
const ShadowNode::Shared& shadowNode) {
if (auto activePointer = getActivePointer(pointerId)) {
// As per the spec this method should silently fail if the pointer in
// question does not have any active buttons
if (activePointer->event.buttons == 0) {
return;
}
pendingPointerCaptureTargetOverrides_[pointerId] = shadowNode;
} else {
// TODO: Throw DOMException with name "NotFoundError" when pointerId does
// not match any of the active pointers
}
}
void PointerEventsProcessor::releasePointerCapture(
PointerIdentifier pointerId,
const ShadowNode* shadowNode) {
if (getActivePointer(pointerId) != nullptr) {
// We only clear the pointer's capture target override if release was called
// on the shadowNode which has the capture override, otherwise the result
// should no-op
auto pendingTarget = getCaptureTargetOverride(
pointerId, pendingPointerCaptureTargetOverrides_);
if (pendingTarget != nullptr &&
pendingTarget->getTag() == shadowNode->getTag()) {
pendingPointerCaptureTargetOverrides_.erase(pointerId);
}
} else {
// TODO: Throw DOMException with name "NotFoundError" when pointerId does
// not match any of the active pointers
}
}
bool PointerEventsProcessor::hasPointerCapture(
PointerIdentifier pointerId,
const ShadowNode* shadowNode) {
ShadowNode::Shared pendingTarget = getCaptureTargetOverride(
pointerId, pendingPointerCaptureTargetOverrides_);
if (pendingTarget != nullptr) {
return pendingTarget->getTag() == shadowNode->getTag();
}
return false;
}
ActivePointer* PointerEventsProcessor::getActivePointer(
PointerIdentifier pointerId) {
auto it = activePointers_.find(pointerId);
return (it == activePointers_.end()) ? nullptr : &it->second;
}
void PointerEventsProcessor::registerActivePointer(const PointerEvent& event) {
ActivePointer activePointer = {};
activePointer.event = event;
// If the pointer has not been tracked by the hover infrastructure then when
// the pointer is released we're gonna have to treat it as if the pointer is
// leaving the screen entirely.
activePointer.shouldLeaveWhenReleased =
previousHoverTrackersPerPointer_.find(event.pointerId) ==
previousHoverTrackersPerPointer_.end();
activePointers_[event.pointerId] = activePointer;
}
void PointerEventsProcessor::updateActivePointer(const PointerEvent& event) {
if (auto activePointer = getActivePointer(event.pointerId)) {
activePointer->event = event;
} else {
LOG(WARNING)
<< "Inconsistency between local and platform pointer registries: attempting to update an active pointer which has never been registered.";
}
}
void PointerEventsProcessor::unregisterActivePointer(
const PointerEvent& event) {
if (getActivePointer(event.pointerId) != nullptr) {
activePointers_.erase(event.pointerId);
} else {
LOG(WARNING)
<< "Inconsistency between local and platform pointer registries: attempting to unregister an active pointer which has never been registered.";
}
}
void PointerEventsProcessor::processPendingPointerCapture(
const PointerEvent& event,
const DispatchEvent& eventDispatcher,
const UIManager& uiManager) {
auto pendingOverride = getCaptureTargetOverride(
event.pointerId, pendingPointerCaptureTargetOverrides_);
bool hasPendingOverride = pendingOverride != nullptr;
auto activeOverride = getCaptureTargetOverride(
event.pointerId, activePointerCaptureTargetOverrides_);
bool hasActiveOverride = activeOverride != nullptr;
if (!hasPendingOverride && !hasActiveOverride) {
return;
}
auto pendingOverrideTag =
(hasPendingOverride) ? pendingOverride->getTag() : -1;
auto activeOverrideTag = (hasActiveOverride) ? activeOverride->getTag() : -1;
if (hasActiveOverride && activeOverrideTag != pendingOverrideTag) {
auto retargeted = retargetPointerEvent(event, *activeOverride, uiManager);
if (shouldEmitPointerEvent(
*retargeted.target, "topLostPointerCapture", uiManager)) {
eventDispatcher(
*retargeted.target,
"topLostPointerCapture",
ReactEventPriority::Discrete,
retargeted.event);
}
}
if (hasPendingOverride && activeOverrideTag != pendingOverrideTag) {
auto retargeted = retargetPointerEvent(event, *pendingOverride, uiManager);
if (shouldEmitPointerEvent(
*retargeted.target, "topGotPointerCapture", uiManager)) {
eventDispatcher(
*retargeted.target,
"topGotPointerCapture",
ReactEventPriority::Discrete,
retargeted.event);
}
}
if (!hasPendingOverride) {
activePointerCaptureTargetOverrides_.erase(event.pointerId);
} else {
activePointerCaptureTargetOverrides_[event.pointerId] = pendingOverride;
}
}
void PointerEventsProcessor::handleIncomingPointerEventOnNode(
const PointerEvent& event,
const ShadowNode::Shared& targetNode,
const DispatchEvent& eventDispatcher,
const UIManager& uiManager) {
// Get the hover tracker from the previous event (default to null if the
// pointer hasn't been tracked before)
auto prevHoverTrackerIt =
previousHoverTrackersPerPointer_.find(event.pointerId);
PointerHoverTracker::Unique prevHoverTracker =
prevHoverTrackerIt != previousHoverTrackersPerPointer_.end()
? std::move(prevHoverTrackerIt->second)
: std::make_unique<PointerHoverTracker>(nullptr, uiManager);
// The previous tracker was stored from a previous tick so we mark it as old
prevHoverTracker->markAsOld();
auto curHoverTracker =
std::make_unique<PointerHoverTracker>(targetNode, uiManager);
// Out
if (!prevHoverTracker->hasSameTarget(*curHoverTracker) &&
prevHoverTracker->areAnyTargetsListeningToEvents(
{ViewEvents::Offset::PointerOut,
ViewEvents::Offset::PointerOutCapture},
uiManager)) {
auto prevTarget = prevHoverTracker->getTarget(uiManager);
if (prevTarget != nullptr) {
eventDispatcher(
*prevTarget, "topPointerOut", ReactEventPriority::Discrete, event);
}
}
// REMINDER: The order of these lists are from the root to the target
const auto [leavingNodes, enteringNodes] =
prevHoverTracker->diffEventPath(*curHoverTracker, uiManager);
// Leaving
// pointerleave events need to be emitted from the deepest target to the root
// but we also need to efficiently keep track of if a view has a parent which
// is listening to the leave events, so we first iterate from the root to the
// target, collecting the views which need events fired for, of which we
// reverse iterate (now from target to root), actually emitting the events.
bool hasParentLeaveCaptureListener = false;
std::vector<std::reference_wrapper<const ShadowNode>> targetsToEmitLeaveTo;
for (auto nodeRef : leavingNodes) {
const auto& node = nodeRef.get();
bool hasCapturingListener = isViewListeningToEvents(
node, {ViewEvents::Offset::PointerLeaveCapture});
bool shouldEmitEvent = hasParentLeaveCaptureListener ||
hasCapturingListener ||
isViewListeningToEvents(node, {ViewEvents::Offset::PointerLeave});
if (shouldEmitEvent) {
targetsToEmitLeaveTo.emplace_back(node);
}
if (hasCapturingListener && !hasParentLeaveCaptureListener) {
hasParentLeaveCaptureListener = true;
}
}
// Actually emit the leave events (in order from target to root)
for (auto it = targetsToEmitLeaveTo.rbegin();
it != targetsToEmitLeaveTo.rend();
it++) {
eventDispatcher(
*it, "topPointerLeave", ReactEventPriority::Discrete, event);
}
// Over
if (!prevHoverTracker->hasSameTarget(*curHoverTracker) &&
curHoverTracker->areAnyTargetsListeningToEvents(
{ViewEvents::Offset::PointerOver,
ViewEvents::Offset::PointerOverCapture},
uiManager)) {
auto curTarget = curHoverTracker->getTarget(uiManager);
if (curTarget != nullptr) {
eventDispatcher(
*curTarget, "topPointerOver", ReactEventPriority::Discrete, event);
}
}
// Entering
// We want to impose the same filtering based on what events are being
// listened to as we did with leaving earlier in this function but we can emit
// the events in this loop inline since it's expected to fire the evens in
// order from root to target.
bool hasParentEnterCaptureListener = false;
for (auto nodeRef : enteringNodes) {
const auto& node = nodeRef.get();
bool hasCapturingListener = isViewListeningToEvents(
node, {ViewEvents::Offset::PointerEnterCapture});
bool shouldEmitEvent = hasParentEnterCaptureListener ||
hasCapturingListener ||
isViewListeningToEvents(node, {ViewEvents::Offset::PointerEnter});
if (shouldEmitEvent) {
eventDispatcher(
node, "topPointerEnter", ReactEventPriority::Discrete, event);
}
if (hasCapturingListener && !hasParentEnterCaptureListener) {
hasParentEnterCaptureListener = true;
}
}
if (targetNode != nullptr) {
previousHoverTrackersPerPointer_[event.pointerId] =
std::move(curHoverTracker);
} else {
previousHoverTrackersPerPointer_.erase(event.pointerId);
}
}
} // namespace facebook::react

View File

@@ -0,0 +1,112 @@
/*
* 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 <functional>
#include <jsi/jsi.h>
#include <react/renderer/uimanager/PointerHoverTracker.h>
#include <react/renderer/uimanager/UIManager.h>
#include <react/renderer/uimanager/primitives.h>
namespace facebook::react {
// Helper struct to package a PointerEvent and SharedEventTarget together
struct PointerEventTarget {
PointerEvent event;
ShadowNode::Shared target;
};
// Helper struct to contain an active pointer's event data along with additional
// metadata
struct ActivePointer {
PointerEvent event;
/*
* Informs the event system that when the touch is released it should be
* treated as the pointer leaving the screen entirely.
*/
bool shouldLeaveWhenReleased{};
};
using DispatchEvent = std::function<void(
const ShadowNode& targetNode,
const std::string& type,
ReactEventPriority priority,
const EventPayload& payload)>;
using PointerIdentifier = int32_t;
using CaptureTargetOverrideRegistry =
std::unordered_map<PointerIdentifier, ShadowNode::Weak>;
using ActivePointerRegistry =
std::unordered_map<PointerIdentifier, ActivePointer>;
using PointerHoverTrackerRegistry =
std::unordered_map<PointerIdentifier, PointerHoverTracker::Unique>;
class PointerEventsProcessor final {
public:
static ShadowNode::Shared getShadowNodeFromEventTarget(
jsi::Runtime& runtime,
const EventTarget* target);
void interceptPointerEvent(
const ShadowNode::Shared& target,
const std::string& type,
ReactEventPriority priority,
const PointerEvent& event,
const DispatchEvent& eventDispatcher,
const UIManager& uiManager);
void setPointerCapture(
PointerIdentifier pointerId,
const ShadowNode::Shared& shadowNode);
void releasePointerCapture(
PointerIdentifier pointerId,
const ShadowNode* shadowNode);
bool hasPointerCapture(
PointerIdentifier pointerId,
const ShadowNode* shadowNode);
private:
ActivePointer* getActivePointer(PointerIdentifier pointerId);
void registerActivePointer(const PointerEvent& event);
void updateActivePointer(const PointerEvent& event);
void unregisterActivePointer(const PointerEvent& event);
void processPendingPointerCapture(
const PointerEvent& event,
const DispatchEvent& eventDispatcher,
const UIManager& uiManager);
ActivePointerRegistry activePointers_;
CaptureTargetOverrideRegistry pendingPointerCaptureTargetOverrides_;
CaptureTargetOverrideRegistry activePointerCaptureTargetOverrides_;
/*
* Private method which is used for tracking the location of pointer events to
* manage the entering/leaving events. The primary idea is that a pointer's
* presence & movement is dicated by a variety of underlying events such as
* down, move, and up — and they should all be treated the same when it comes
* to tracking the entering & leaving of pointers to views. This method
* accomplishes that by receiving the pointer event, and the target view (can
* be null in cases when the event indicates that the pointer has left the
* screen entirely)
*/
void handleIncomingPointerEventOnNode(
const PointerEvent& event,
const ShadowNode::Shared& targetNode,
const DispatchEvent& eventDispatcher,
const UIManager& uiManager);
PointerHoverTrackerRegistry previousHoverTrackersPerPointer_;
};
} // namespace facebook::react

View File

@@ -0,0 +1,150 @@
/*
* 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 "PointerHoverTracker.h"
#include <ranges>
#include <utility>
namespace facebook::react {
using EventPath = PointerHoverTracker::EventPath;
PointerHoverTracker::PointerHoverTracker(
ShadowNode::Shared target,
const UIManager& uiManager)
: target_(std::move(target)) {
if (target_ != nullptr) {
// Retrieve the root shadow node at this current revision so that we can
// leverage it to get the event path list at the moment the event occured
auto rootShadowNode = ShadowNode::Shared{};
auto& shadowTreeRegistry = uiManager.getShadowTreeRegistry();
shadowTreeRegistry.visit(
target_->getSurfaceId(),
[&rootShadowNode](const ShadowTree& shadowTree) {
rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode;
});
this->root_ = rootShadowNode;
}
}
bool PointerHoverTracker::hasSameTarget(
const PointerHoverTracker& other) const {
if (target_ != nullptr && other.target_ != nullptr) {
return ShadowNode::sameFamily(*this->target_, *other.target_);
}
return false;
}
bool PointerHoverTracker::areAnyTargetsListeningToEvents(
std::initializer_list<ViewEvents::Offset> eventTypes,
const UIManager& uiManager) const {
auto eventPath = getEventPathTargets();
for (const auto& oldTarget : eventPath) {
auto newestTarget = uiManager.getNewestCloneOfShadowNode(oldTarget);
if (newestTarget &&
newestTarget->getTraits().check(ShadowNodeTraits::Trait::ViewKind)) {
auto eventFlags =
static_cast<const ViewProps&>(*newestTarget->getProps()).events;
for (const auto& eventType : eventTypes) {
if (eventFlags[eventType]) {
return true;
}
}
}
}
return false;
}
std::tuple<EventPath, EventPath> PointerHoverTracker::diffEventPath(
const PointerHoverTracker& other,
const UIManager& uiManager) const {
auto myEventPath = getEventPathTargets();
auto otherEventPath = other.getEventPathTargets();
// Starting from the root node, iterate through both event paths, comparing
// the nodes' families until a difference is found, and then just break out of
// the loop early. This will leave the iterators for each path at the point
// where the event paths diverge and can be subsequently used as the beginning
// iterator of a subrange, where the subrange on *this* tracker would
// represent the removed views, and the subrange on *other* tracker would
// represent the added views.
//
// NOTE: This works based on the assumption that nodes in react-native don't
// get "re-parented" so if there are any bugs reported due to extra
// leave->enter events, this solution may need to be revisited with a more
// robust diffing solution.
auto myIt = myEventPath.rbegin();
auto otherIt = otherEventPath.rbegin();
while (myIt != myEventPath.rend() && otherIt != otherEventPath.rend()) {
if (!ShadowNode::sameFamily(myIt->get(), otherIt->get())) {
break;
}
++myIt;
++otherIt;
}
EventPath removed;
for (auto nodeIt = myIt; nodeIt != myEventPath.rend(); nodeIt++) {
const auto& latestNode = getLatestNode(*nodeIt, uiManager);
if (latestNode != nullptr) {
removed.push_back(*latestNode);
}
}
EventPath added;
for (auto nodeIt = otherIt; nodeIt != otherEventPath.rend(); nodeIt++) {
const auto& latestNode = other.getLatestNode(*nodeIt, uiManager);
if (latestNode != nullptr) {
added.push_back(*latestNode);
}
}
return std::make_tuple(removed, added);
}
const ShadowNode* PointerHoverTracker::getTarget(
const UIManager& uiManager) const {
if (target_ == nullptr) {
return nullptr;
}
return getLatestNode(*target_, uiManager);
}
void PointerHoverTracker::markAsOld() {
isOldTracker_ = true;
}
const ShadowNode* PointerHoverTracker::getLatestNode(
const ShadowNode& node,
const UIManager& uiManager) const {
if (isOldTracker_) {
auto newestTarget = uiManager.getNewestCloneOfShadowNode(node);
return newestTarget.get();
}
return &node;
}
EventPath PointerHoverTracker::getEventPathTargets() const {
EventPath result{};
if (target_ == nullptr || root_ == nullptr) {
return result;
}
auto ancestors = target_->getFamily().getAncestors(*root_);
result.emplace_back(*target_);
for (auto it = ancestors.rbegin(); it != ancestors.rend(); it++) {
result.push_back(it->first);
}
return result;
}
} // namespace facebook::react

View File

@@ -0,0 +1,71 @@
/*
* 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 <functional>
#include <initializer_list>
#include <memory>
#include <react/renderer/components/view/primitives.h>
#include <react/renderer/core/ReactPrimitives.h>
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/uimanager/UIManager.h>
namespace facebook::react {
class PointerHoverTracker {
public:
using Unique = std::unique_ptr<PointerHoverTracker>;
using EventPath = std::vector<std::reference_wrapper<const ShadowNode>>;
PointerHoverTracker(ShadowNode::Shared target, const UIManager& uiManager);
const ShadowNode* getTarget(const UIManager& uiManager) const;
bool hasSameTarget(const PointerHoverTracker& other) const;
bool areAnyTargetsListeningToEvents(
std::initializer_list<ViewEvents::Offset> eventTypes,
const UIManager& uiManager) const;
/**
* Performs a diff between the current and given trackers and returns a tuple
* containing [1]: the nodes that have been removed and [2]: the nodes that
* have been added. Note that the order of these lists are from parents ->
* children.
*/
std::tuple<EventPath, EventPath> diffEventPath(
const PointerHoverTracker& other,
const UIManager& uiManager) const;
void markAsOld();
private:
/**
* Flag that lets the tracker know if it needs to fetch the latest version of
* the shadow node or not.
*/
bool isOldTracker_ = false;
ShadowNode::Shared root_;
ShadowNode::Shared target_;
/**
* A thin wrapper around `UIManager::getNewestCloneOfShadowNode` that only
* actually gets the newest clone only if the tracker is marked as "old".
*/
const ShadowNode* getLatestNode(
const ShadowNode& node,
const UIManager& uiManager) const;
/**
* Retrieves the list of shadow node references in the event's path starting
* from the target node to the root node.
*/
EventPath getEventPathTargets() const;
};
} // namespace facebook::react

View File

@@ -0,0 +1,123 @@
/*
* 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 "SurfaceRegistryBinding.h"
#include <react/renderer/debug/SystraceSection.h>
#include <react/renderer/uimanager/bindingUtils.h>
#include <react/renderer/uimanager/primitives.h>
#include "bindingUtils.h"
namespace facebook::react {
namespace {
void throwIfBridgeless(
jsi::Runtime& runtime,
jsi::Object& global,
const char* methodName) {
auto isBridgeless = global.getProperty(runtime, "RN$Bridgeless");
if (isBridgeless.isBool() && isBridgeless.asBool()) {
throw std::runtime_error(
"SurfaceRegistryBinding::" + std::string(methodName) +
" failed. Global was not installed.");
}
}
} // namespace
void SurfaceRegistryBinding::startSurface(
jsi::Runtime& runtime,
SurfaceId surfaceId,
const std::string& moduleName,
const folly::dynamic& initialProps,
DisplayMode displayMode) {
SystraceSection s("SurfaceRegistryBinding::startSurface");
jsi::Object parameters(runtime);
parameters.setProperty(runtime, "rootTag", surfaceId);
parameters.setProperty(
runtime, "initialProps", jsi::valueFromDynamic(runtime, initialProps));
parameters.setProperty(runtime, "fabric", true);
auto global = runtime.global();
auto registry = global.getProperty(runtime, "RN$AppRegistry");
if (registry.isObject()) {
auto method = std::move(registry).asObject(runtime).getPropertyAsFunction(
runtime, "runApplication");
method.call(
runtime,
{jsi::String::createFromUtf8(runtime, moduleName),
std::move(parameters),
jsi::Value(runtime, displayModeToInt(displayMode))});
} else {
throwIfBridgeless(runtime, global, "startSurface");
callMethodOfModule(
runtime,
"AppRegistry",
"runApplication",
{jsi::String::createFromUtf8(runtime, moduleName),
std::move(parameters),
jsi::Value(runtime, displayModeToInt(displayMode))});
}
}
void SurfaceRegistryBinding::setSurfaceProps(
jsi::Runtime& runtime,
SurfaceId surfaceId,
const std::string& moduleName,
const folly::dynamic& initialProps,
DisplayMode displayMode) {
SystraceSection s("UIManagerBinding::setSurfaceProps");
jsi::Object parameters(runtime);
parameters.setProperty(runtime, "rootTag", surfaceId);
parameters.setProperty(
runtime, "initialProps", jsi::valueFromDynamic(runtime, initialProps));
parameters.setProperty(runtime, "fabric", true);
auto global = runtime.global();
auto registry = global.getProperty(runtime, "RN$AppRegistry");
if (registry.isObject()) {
auto method = std::move(registry).asObject(runtime).getPropertyAsFunction(
runtime, "setSurfaceProps");
method.call(
runtime,
{jsi::String::createFromUtf8(runtime, moduleName),
std::move(parameters),
jsi::Value(runtime, displayModeToInt(displayMode))});
} else {
throwIfBridgeless(runtime, global, "setSurfaceProps");
callMethodOfModule(
runtime,
"AppRegistry",
"setSurfaceProps",
{jsi::String::createFromUtf8(runtime, moduleName),
std::move(parameters),
jsi::Value(runtime, displayModeToInt(displayMode))});
}
}
void SurfaceRegistryBinding::stopSurface(
jsi::Runtime& runtime,
SurfaceId surfaceId) {
auto global = runtime.global();
auto stopFunction = global.getProperty(runtime, "RN$stopSurface");
if (stopFunction.isObject() &&
stopFunction.asObject(runtime).isFunction(runtime)) {
std::move(stopFunction)
.asObject(runtime)
.asFunction(runtime)
.call(runtime, {jsi::Value{surfaceId}});
} else {
throwIfBridgeless(runtime, global, "stopSurface");
callMethodOfModule(
runtime,
"ReactFabric",
"unmountComponentAtNode",
{jsi::Value{surfaceId}});
}
}
} // namespace facebook::react

View File

@@ -0,0 +1,49 @@
/*
* 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 <jsi/jsi.h>
#include <react/renderer/core/ReactPrimitives.h>
namespace facebook::react {
class SurfaceRegistryBinding final {
public:
SurfaceRegistryBinding() = delete;
/*
* Starts React Native Surface with given id, moduleName, and props.
* Thread synchronization must be enforced externally.
*/
static void startSurface(
jsi::Runtime& runtime,
SurfaceId surfaceId,
const std::string& moduleName,
const folly::dynamic& initialProps,
DisplayMode displayMode);
/*
* Updates the React Native Surface identified with surfaceId and moduleName
* with the given props.
* Thread synchronization must be enforced externally.
*/
static void setSurfaceProps(
jsi::Runtime& runtime,
SurfaceId surfaceId,
const std::string& moduleName,
const folly::dynamic& initialProps,
DisplayMode displayMode);
/*
* Stops React Native Surface with given id.
* Thread synchronization must be enforced externally.
*/
static void stopSurface(jsi::Runtime& runtime, SurfaceId surfaceId);
};
} // namespace facebook::react

View File

@@ -0,0 +1,757 @@
/*
* 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 "UIManager.h"
#include <cxxreact/JSExecutor.h>
#include <react/debug/react_native_assert.h>
#include <react/renderer/core/DynamicPropsUtilities.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/core/ShadowNodeFragment.h>
#include <react/renderer/debug/SystraceSection.h>
#include <react/renderer/uimanager/SurfaceRegistryBinding.h>
#include <react/renderer/uimanager/UIManagerBinding.h>
#include <react/renderer/uimanager/UIManagerCommitHook.h>
#include <react/renderer/uimanager/UIManagerMountHook.h>
#include <glog/logging.h>
#include <utility>
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;
} // namespace
namespace facebook::react {
// Explicitly define destructors here, as they to exist in order to act as a
// "key function" for the ShadowNodeWrapper class -- this allow for RTTI to work
// properly across dynamic library boundaries (i.e. dynamic_cast that is used by
// isHostObject method)
ShadowNodeListWrapper::~ShadowNodeListWrapper() = default;
static std::unique_ptr<LeakChecker> constructLeakCheckerIfNeeded(
const RuntimeExecutor& runtimeExecutor) {
#ifdef REACT_NATIVE_DEBUG
return std::make_unique<LeakChecker>(runtimeExecutor);
#else
return {};
#endif
}
UIManager::UIManager(
const RuntimeExecutor& runtimeExecutor,
BackgroundExecutor backgroundExecutor,
ContextContainer::Shared contextContainer)
: runtimeExecutor_(runtimeExecutor),
backgroundExecutor_(std::move(backgroundExecutor)),
contextContainer_(std::move(contextContainer)),
leakChecker_(constructLeakCheckerIfNeeded(runtimeExecutor)) {}
UIManager::~UIManager() {
LOG(WARNING) << "UIManager::~UIManager() was called (address: " << this
<< ").";
}
std::shared_ptr<ShadowNode> UIManager::createNode(
Tag tag,
const std::string& name,
SurfaceId surfaceId,
RawProps rawProps,
InstanceHandle::Shared instanceHandle) const {
SystraceSection s("UIManager::createNode", "componentName", name);
auto& componentDescriptor = componentDescriptorRegistry_->at(name);
auto fallbackDescriptor =
componentDescriptorRegistry_->getFallbackComponentDescriptor();
PropsParserContext propsParserContext{surfaceId, *contextContainer_.get()};
auto family = componentDescriptor.createFamily(
{tag, surfaceId, std::move(instanceHandle)});
const auto props = componentDescriptor.cloneProps(
propsParserContext, nullptr, std::move(rawProps));
const auto state = componentDescriptor.createInitialState(props, family);
auto shadowNode = componentDescriptor.createShadowNode(
ShadowNodeFragment{
/* .props = */
fallbackDescriptor != nullptr &&
fallbackDescriptor->getComponentHandle() ==
componentDescriptor.getComponentHandle()
? componentDescriptor.cloneProps(
propsParserContext,
props,
RawProps(folly::dynamic::object("name", name)))
: props,
/* .children = */ ShadowNodeFragment::childrenPlaceholder(),
/* .state = */ state,
},
family);
if (delegate_ != nullptr) {
delegate_->uiManagerDidCreateShadowNode(*shadowNode);
}
if (leakChecker_) {
leakChecker_->uiManagerDidCreateShadowNodeFamily(family);
}
return shadowNode;
}
std::shared_ptr<ShadowNode> UIManager::cloneNode(
const ShadowNode& shadowNode,
const ShadowNode::SharedListOfShared& children,
RawProps rawProps) const {
SystraceSection s(
"UIManager::cloneNode", "componentName", shadowNode.getComponentName());
PropsParserContext propsParserContext{
shadowNode.getFamily().getSurfaceId(), *contextContainer_.get()};
auto& componentDescriptor = shadowNode.getComponentDescriptor();
auto& family = shadowNode.getFamily();
auto props = ShadowNodeFragment::propsPlaceholder();
if (!rawProps.isEmpty()) {
if (family.nativeProps_DEPRECATED != nullptr) {
// Values in `rawProps` patch (take precedence over)
// `nativeProps_DEPRECATED`. For example, if both `nativeProps_DEPRECATED`
// and `rawProps` contain key 'A'. Value from `rawProps` overrides what
// was previously in `nativeProps_DEPRECATED`.
family.nativeProps_DEPRECATED =
std::make_unique<folly::dynamic>(mergeDynamicProps(
*family.nativeProps_DEPRECATED, (folly::dynamic)rawProps));
props = componentDescriptor.cloneProps(
propsParserContext,
shadowNode.getProps(),
RawProps(*family.nativeProps_DEPRECATED));
} else {
props = componentDescriptor.cloneProps(
propsParserContext, shadowNode.getProps(), std::move(rawProps));
}
}
auto clonedShadowNode = componentDescriptor.cloneShadowNode(
shadowNode,
{
/* .props = */ props,
/* .children = */ children,
});
return clonedShadowNode;
}
void UIManager::appendChild(
const ShadowNode::Shared& parentShadowNode,
const ShadowNode::Shared& childShadowNode) const {
SystraceSection s("UIManager::appendChild");
auto& componentDescriptor = parentShadowNode->getComponentDescriptor();
componentDescriptor.appendChild(parentShadowNode, childShadowNode);
}
void UIManager::completeSurface(
SurfaceId surfaceId,
const ShadowNode::UnsharedListOfShared& rootChildren,
ShadowTree::CommitOptions commitOptions) const {
SystraceSection s("UIManager::completeSurface", "surfaceId", surfaceId);
shadowTreeRegistry_.visit(surfaceId, [&](const ShadowTree& shadowTree) {
shadowTree.commit(
[&](RootShadowNode const& oldRootShadowNode) {
return std::make_shared<RootShadowNode>(
oldRootShadowNode,
ShadowNodeFragment{
.props = ShadowNodeFragment::propsPlaceholder(),
.children = rootChildren,
});
},
commitOptions);
});
}
void UIManager::setIsJSResponder(
const ShadowNode::Shared& shadowNode,
bool isJSResponder,
bool blockNativeResponder) const {
if (delegate_ != nullptr) {
delegate_->uiManagerDidSetIsJSResponder(
shadowNode, isJSResponder, blockNativeResponder);
}
}
void UIManager::startSurface(
ShadowTree::Unique&& shadowTree,
const std::string& moduleName,
const folly::dynamic& props,
DisplayMode displayMode) const {
SystraceSection s("UIManager::startSurface");
auto surfaceId = shadowTree->getSurfaceId();
shadowTreeRegistry_.add(std::move(shadowTree));
runtimeExecutor_([=](jsi::Runtime& runtime) {
SystraceSection s("UIManager::startSurface::onRuntime");
SurfaceRegistryBinding::startSurface(
runtime, surfaceId, moduleName, props, displayMode);
});
}
void UIManager::setSurfaceProps(
SurfaceId surfaceId,
const std::string& moduleName,
const folly::dynamic& props,
DisplayMode displayMode) const {
SystraceSection s("UIManager::setSurfaceProps");
runtimeExecutor_([=](jsi::Runtime& runtime) {
SurfaceRegistryBinding::setSurfaceProps(
runtime, surfaceId, moduleName, props, displayMode);
});
}
ShadowTree::Unique UIManager::stopSurface(SurfaceId surfaceId) const {
SystraceSection s("UIManager::stopSurface");
// Stop any ongoing animations.
stopSurfaceForAnimationDelegate(surfaceId);
// Waiting for all concurrent commits to be finished and unregistering the
// `ShadowTree`.
auto shadowTree = getShadowTreeRegistry().remove(surfaceId);
if (shadowTree) {
// We execute JavaScript/React part of the process at the very end to
// minimize any visible side-effects of stopping the Surface. Any possible
// commits from the JavaScript side will not be able to reference a
// `ShadowTree` and will fail silently.
runtimeExecutor_([=](jsi::Runtime& runtime) {
SurfaceRegistryBinding::stopSurface(runtime, surfaceId);
});
if (leakChecker_) {
leakChecker_->stopSurface(surfaceId);
}
}
return shadowTree;
}
ShadowNode::Shared UIManager::getNewestCloneOfShadowNode(
const ShadowNode& shadowNode) const {
auto ancestorShadowNode = ShadowNode::Shared{};
shadowTreeRegistry_.visit(
shadowNode.getSurfaceId(), [&](const ShadowTree& shadowTree) {
ancestorShadowNode = shadowTree.getCurrentRevision().rootShadowNode;
});
if (!ancestorShadowNode) {
return nullptr;
}
// If the given shadow node is of the same family as the root shadow node,
// return the latest root shadow node
if (ShadowNode::sameFamily(*ancestorShadowNode, shadowNode)) {
return ancestorShadowNode;
}
auto ancestors = shadowNode.getFamily().getAncestors(*ancestorShadowNode);
if (ancestors.empty()) {
return nullptr;
}
auto pair = ancestors.rbegin();
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<const LayoutableShadowNode*>(&(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;
}
ShadowNode::Shared UIManager::findNodeAtPoint(
const ShadowNode::Shared& node,
Point point) const {
return LayoutableShadowNode::findNodeAtPoint(
getNewestCloneOfShadowNode(*node), point);
}
LayoutMetrics UIManager::getRelativeLayoutMetrics(
const ShadowNode& shadowNode,
const ShadowNode* ancestorShadowNode,
LayoutableShadowNode::LayoutInspectingPolicy policy) const {
SystraceSection s("UIManager::getRelativeLayoutMetrics");
// We might store here an owning pointer to `ancestorShadowNode` to ensure
// that the node is not deallocated during method execution lifetime.
auto owningAncestorShadowNode = ShadowNode::Shared{};
if (ancestorShadowNode == nullptr) {
shadowTreeRegistry_.visit(
shadowNode.getSurfaceId(), [&](const ShadowTree& shadowTree) {
owningAncestorShadowNode =
shadowTree.getCurrentRevision().rootShadowNode;
ancestorShadowNode = owningAncestorShadowNode.get();
});
} else {
// It is possible for JavaScript (or other callers) to have a reference
// to a previous version of ShadowNodes, but we enforce that
// metrics are only calculated on most recently committed versions.
owningAncestorShadowNode = getNewestCloneOfShadowNode(*ancestorShadowNode);
ancestorShadowNode = owningAncestorShadowNode.get();
}
auto layoutableAncestorShadowNode =
dynamic_cast<const LayoutableShadowNode*>(ancestorShadowNode);
if (layoutableAncestorShadowNode == nullptr) {
return EmptyLayoutMetrics;
}
return LayoutableShadowNode::computeRelativeLayoutMetrics(
shadowNode.getFamily(), *layoutableAncestorShadowNode, policy);
}
void UIManager::updateState(const StateUpdate& stateUpdate) const {
SystraceSection s(
"UIManager::updateState",
"componentName",
stateUpdate.family->getComponentName());
auto& callback = stateUpdate.callback;
auto& family = stateUpdate.family;
auto& componentDescriptor = family->getComponentDescriptor();
shadowTreeRegistry_.visit(
family->getSurfaceId(), [&](const ShadowTree& shadowTree) {
shadowTree.commit(
[&](RootShadowNode const& oldRootShadowNode) {
auto isValid = true;
auto rootNode = oldRootShadowNode.cloneTree(
*family, [&](ShadowNode const& oldShadowNode) {
auto newData =
callback(oldShadowNode.getState()->getDataPointer());
if (!newData) {
isValid = false;
// Just return something, we will discard it anyway.
return oldShadowNode.clone({});
}
auto newState =
componentDescriptor.createState(*family, newData);
return oldShadowNode.clone({
/* .props = */ ShadowNodeFragment::propsPlaceholder(),
/* .children = */
ShadowNodeFragment::childrenPlaceholder(),
/* .state = */ newState,
});
});
return isValid
? std::static_pointer_cast<RootShadowNode>(rootNode)
: nullptr;
},
{/* default commit options */});
});
}
void UIManager::dispatchCommand(
const ShadowNode::Shared& shadowNode,
const std::string& commandName,
const folly::dynamic& args) const {
if (delegate_ != nullptr) {
delegate_->uiManagerDidDispatchCommand(shadowNode, commandName, args);
}
}
void UIManager::setNativeProps_DEPRECATED(
const ShadowNode::Shared& shadowNode,
RawProps rawProps) const {
auto& family = shadowNode->getFamily();
if (family.nativeProps_DEPRECATED) {
// Values in `rawProps` patch (take precedence over)
// `nativeProps_DEPRECATED`. For example, if both `nativeProps_DEPRECATED`
// and `rawProps` contain key 'A'. Value from `rawProps` overrides what was
// previously in `nativeProps_DEPRECATED`.
family.nativeProps_DEPRECATED =
std::make_unique<folly::dynamic>(mergeDynamicProps(
*family.nativeProps_DEPRECATED, (folly::dynamic)rawProps));
} else {
family.nativeProps_DEPRECATED =
std::make_unique<folly::dynamic>((folly::dynamic)rawProps);
}
shadowTreeRegistry_.visit(
family.getSurfaceId(), [&](const ShadowTree& shadowTree) {
// The lambda passed to `commit` may be executed multiple times.
// We need to create fresh copy of the `RawProps` object each time.
shadowTree.commit(
[&](RootShadowNode const& oldRootShadowNode) {
auto rootNode = oldRootShadowNode.cloneTree(
family, [&](ShadowNode const& oldShadowNode) {
auto& componentDescriptor =
componentDescriptorRegistry_->at(
shadowNode->getComponentHandle());
PropsParserContext propsParserContext{
family.getSurfaceId(), *contextContainer_.get()};
auto props = componentDescriptor.cloneProps(
propsParserContext,
getNewestCloneOfShadowNode(*shadowNode)->getProps(),
RawProps(rawProps));
return oldShadowNode.clone({/* .props = */ props});
});
return std::static_pointer_cast<RootShadowNode>(rootNode);
},
{/* default commit options */});
});
}
void UIManager::sendAccessibilityEvent(
const ShadowNode::Shared& shadowNode,
const std::string& eventType) {
if (delegate_ != nullptr) {
delegate_->uiManagerDidSendAccessibilityEvent(shadowNode, eventType);
}
}
void UIManager::configureNextLayoutAnimation(
jsi::Runtime& runtime,
const RawValue& config,
const jsi::Value& successCallback,
const jsi::Value& failureCallback) const {
if (animationDelegate_ != nullptr) {
animationDelegate_->uiManagerDidConfigureNextLayoutAnimation(
runtime,
config,
std::move(successCallback),
std::move(failureCallback));
}
}
static ShadowNode::Shared findShadowNodeByTagRecursively(
ShadowNode::Shared parentShadowNode,
Tag tag) {
if (parentShadowNode->getTag() == tag) {
return parentShadowNode;
}
for (const ShadowNode::Shared& shadowNode : parentShadowNode->getChildren()) {
auto result = findShadowNodeByTagRecursively(shadowNode, tag);
if (result) {
return result;
}
}
return nullptr;
}
ShadowNode::Shared UIManager::findShadowNodeByTag_DEPRECATED(Tag tag) const {
auto shadowNode = ShadowNode::Shared{};
shadowTreeRegistry_.enumerate([&](const ShadowTree& shadowTree, bool& stop) {
RootShadowNode const* rootShadowNode;
// The public interface of `ShadowTree` discourages accessing a stored
// pointer to a root node because of the possible data race.
// To work around this, we ask for a commit and immediately cancel it
// returning `nullptr` instead of a new shadow tree.
// We don't want to add a way to access a stored pointer to a root node
// because this `findShadowNodeByTag` is deprecated. It is only added
// to make migration to the new architecture easier.
shadowTree.tryCommit(
[&](RootShadowNode const& oldRootShadowNode) {
rootShadowNode = &oldRootShadowNode;
return nullptr;
},
{/* default commit options */});
if (rootShadowNode != nullptr) {
auto const& children = rootShadowNode->getChildren();
if (!children.empty()) {
auto const& child = children.front();
shadowNode = findShadowNodeByTagRecursively(child, tag);
if (shadowNode) {
stop = true;
}
}
}
});
return shadowNode;
}
void UIManager::setComponentDescriptorRegistry(
const SharedComponentDescriptorRegistry& componentDescriptorRegistry) {
componentDescriptorRegistry_ = componentDescriptorRegistry;
}
void UIManager::setDelegate(UIManagerDelegate* delegate) {
delegate_ = delegate;
}
UIManagerDelegate* UIManager::getDelegate() {
return delegate_;
}
void UIManager::visitBinding(
const std::function<void(UIManagerBinding const& uiManagerBinding)>&
callback,
jsi::Runtime& runtime) const {
auto uiManagerBinding = UIManagerBinding::getBinding(runtime);
if (uiManagerBinding) {
callback(*uiManagerBinding);
}
}
const ShadowTreeRegistry& UIManager::getShadowTreeRegistry() const {
return shadowTreeRegistry_;
}
void UIManager::registerCommitHook(UIManagerCommitHook& commitHook) {
std::unique_lock lock(commitHookMutex_);
react_native_assert(
std::find(commitHooks_.begin(), commitHooks_.end(), &commitHook) ==
commitHooks_.end());
commitHook.commitHookWasRegistered(*this);
commitHooks_.push_back(&commitHook);
}
void UIManager::unregisterCommitHook(UIManagerCommitHook& commitHook) {
std::unique_lock lock(commitHookMutex_);
auto iterator =
std::find(commitHooks_.begin(), commitHooks_.end(), &commitHook);
react_native_assert(iterator != commitHooks_.end());
commitHooks_.erase(iterator);
commitHook.commitHookWasUnregistered(*this);
}
void UIManager::registerMountHook(UIManagerMountHook& mountHook) {
std::unique_lock lock(mountHookMutex_);
react_native_assert(
std::find(mountHooks_.begin(), mountHooks_.end(), &mountHook) ==
mountHooks_.end());
mountHooks_.push_back(&mountHook);
}
void UIManager::unregisterMountHook(UIManagerMountHook& mountHook) {
std::unique_lock lock(mountHookMutex_);
auto iterator = std::find(mountHooks_.begin(), mountHooks_.end(), &mountHook);
react_native_assert(iterator != mountHooks_.end());
mountHooks_.erase(iterator);
}
#pragma mark - ShadowTreeDelegate
RootShadowNode::Unshared UIManager::shadowTreeWillCommit(
const ShadowTree& shadowTree,
const RootShadowNode::Shared& oldRootShadowNode,
const RootShadowNode::Unshared& newRootShadowNode) const {
SystraceSection s("UIManager::shadowTreeWillCommit");
std::shared_lock lock(commitHookMutex_);
auto resultRootShadowNode = newRootShadowNode;
for (auto* commitHook : commitHooks_) {
resultRootShadowNode = commitHook->shadowTreeWillCommit(
shadowTree, oldRootShadowNode, resultRootShadowNode);
}
return resultRootShadowNode;
}
void UIManager::shadowTreeDidFinishTransaction(
MountingCoordinator::Shared mountingCoordinator,
bool mountSynchronously) const {
SystraceSection s("UIManager::shadowTreeDidFinishTransaction");
if (delegate_ != nullptr) {
delegate_->uiManagerDidFinishTransaction(
std::move(mountingCoordinator), mountSynchronously);
}
}
void UIManager::reportMount(SurfaceId surfaceId) const {
SystraceSection s("UIManager::reportMount");
auto time = JSExecutor::performanceNow();
auto rootShadowNode = RootShadowNode::Shared{};
shadowTreeRegistry_.visit(surfaceId, [&](const ShadowTree& shadowTree) {
rootShadowNode =
shadowTree.getMountingCoordinator()->getBaseRevision().rootShadowNode;
});
if (!rootShadowNode) {
return;
}
{
std::shared_lock lock(mountHookMutex_);
for (auto* mountHook : mountHooks_) {
mountHook->shadowTreeDidMount(rootShadowNode, time);
}
}
}
#pragma mark - UIManagerAnimationDelegate
void UIManager::setAnimationDelegate(UIManagerAnimationDelegate* delegate) {
animationDelegate_ = delegate;
}
void UIManager::stopSurfaceForAnimationDelegate(SurfaceId surfaceId) const {
if (animationDelegate_ != nullptr) {
animationDelegate_->stopSurface(surfaceId);
}
}
void UIManager::animationTick() const {
if (animationDelegate_ != nullptr &&
animationDelegate_->shouldAnimateFrame()) {
shadowTreeRegistry_.enumerate([](const ShadowTree& shadowTree, bool&) {
shadowTree.notifyDelegatesOfUpdates();
});
}
}
} // namespace facebook::react

View File

@@ -0,0 +1,243 @@
/*
* 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 <folly/dynamic.h>
#include <jsi/jsi.h>
#include <ReactCommon/RuntimeExecutor.h>
#include <shared_mutex>
#include <react/renderer/componentregistry/ComponentDescriptorRegistry.h>
#include <react/renderer/core/InstanceHandle.h>
#include <react/renderer/core/RawValue.h>
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/core/StateData.h>
#include <react/renderer/leakchecker/LeakChecker.h>
#include <react/renderer/mounting/ShadowTree.h>
#include <react/renderer/mounting/ShadowTreeDelegate.h>
#include <react/renderer/mounting/ShadowTreeRegistry.h>
#include <react/renderer/uimanager/UIManagerAnimationDelegate.h>
#include <react/renderer/uimanager/UIManagerDelegate.h>
#include <react/renderer/uimanager/primitives.h>
#include <react/utils/ContextContainer.h>
namespace facebook::react {
class UIManagerBinding;
class UIManagerCommitHook;
class UIManagerMountHook;
class UIManager final : public ShadowTreeDelegate {
public:
UIManager(
const RuntimeExecutor& runtimeExecutor,
BackgroundExecutor backgroundExecutor,
ContextContainer::Shared contextContainer);
~UIManager() override;
void setComponentDescriptorRegistry(
const SharedComponentDescriptorRegistry& componentDescriptorRegistry);
/*
* Sets and gets the UIManager's delegate.
* The delegate is stored as a raw pointer, so the owner must null
* the pointer before being destroyed.
*/
void setDelegate(UIManagerDelegate* delegate);
UIManagerDelegate* getDelegate();
/**
* Sets and gets the UIManager's Animation APIs delegate.
* The delegate is stored as a raw pointer, so the owner must null
* the pointer before being destroyed.
*/
void setAnimationDelegate(UIManagerAnimationDelegate* delegate);
/**
* Execute stopSurface on any UIMAnagerAnimationDelegate.
*/
void stopSurfaceForAnimationDelegate(SurfaceId surfaceId) const;
void animationTick() const;
/*
* Provides access to a UIManagerBindging.
* The `callback` methods will not be called if the internal pointer to
* `UIManagerBindging` is `nullptr`.
* The callback is called synchronously on the same thread.
*/
void visitBinding(
const std::function<void(UIManagerBinding const& uiManagerBinding)>&
callback,
jsi::Runtime& runtime) const;
/*
* Registers and unregisters a commit hook.
*/
void registerCommitHook(UIManagerCommitHook& commitHook);
void unregisterCommitHook(UIManagerCommitHook& commitHook);
/*
* Registers and unregisters a mount hook.
*/
void registerMountHook(UIManagerMountHook& mountHook);
void unregisterMountHook(UIManagerMountHook& mountHook);
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;
#pragma mark - Surface Start & Stop
void startSurface(
ShadowTree::Unique&& shadowTree,
const std::string& moduleName,
const folly::dynamic& props,
DisplayMode displayMode) const;
void setSurfaceProps(
SurfaceId surfaceId,
const std::string& moduleName,
const folly::dynamic& props,
DisplayMode displayMode) const;
ShadowTree::Unique stopSurface(SurfaceId surfaceId) const;
#pragma mark - ShadowTreeDelegate
void shadowTreeDidFinishTransaction(
MountingCoordinator::Shared mountingCoordinator,
bool mountSynchronously) const override;
RootShadowNode::Unshared shadowTreeWillCommit(
const ShadowTree& shadowTree,
const RootShadowNode::Shared& oldRootShadowNode,
const RootShadowNode::Unshared& newRootShadowNode) const override;
std::shared_ptr<ShadowNode> createNode(
Tag tag,
const std::string& componentName,
SurfaceId surfaceId,
RawProps props,
InstanceHandle::Shared instanceHandle) const;
std::shared_ptr<ShadowNode> cloneNode(
const ShadowNode& shadowNode,
const ShadowNode::SharedListOfShared& children,
RawProps rawProps) const;
void appendChild(
const ShadowNode::Shared& parentShadowNode,
const ShadowNode::Shared& childShadowNode) const;
void completeSurface(
SurfaceId surfaceId,
const ShadowNode::UnsharedListOfShared& rootChildren,
ShadowTree::CommitOptions commitOptions) const;
void setIsJSResponder(
const ShadowNode::Shared& shadowNode,
bool isJSResponder,
bool blockNativeResponder) const;
ShadowNode::Shared findNodeAtPoint(
const ShadowNode::Shared& shadowNode,
Point point) const;
/*
* Returns layout metrics of given `shadowNode` relative to
* `ancestorShadowNode` (relative to the root node in case if provided
* `ancestorShadowNode` is nullptr).
*/
LayoutMetrics getRelativeLayoutMetrics(
const ShadowNode& shadowNode,
const ShadowNode* ancestorShadowNode,
LayoutableShadowNode::LayoutInspectingPolicy policy) const;
/*
* Creates a new shadow node with given state data, clones what's necessary
* and performs a commit.
*/
void updateState(const StateUpdate& stateUpdate) const;
void dispatchCommand(
const ShadowNode::Shared& shadowNode,
const std::string& commandName,
const folly::dynamic& args) const;
void setNativeProps_DEPRECATED(
const ShadowNode::Shared& shadowNode,
RawProps rawProps) const;
void sendAccessibilityEvent(
const ShadowNode::Shared& shadowNode,
const std::string& eventType);
/*
* Iterates over all shadow nodes which are parts of all registered surfaces
* and find the one that has given `tag`. Returns `nullptr` if the node wasn't
* found. This is a temporary workaround that should not be used in any core
* functionality.
*/
ShadowNode::Shared findShadowNodeByTag_DEPRECATED(Tag tag) const;
const ShadowTreeRegistry& getShadowTreeRegistry() const;
void reportMount(SurfaceId surfaceId) const;
bool hasBackgroundExecutor() const {
return backgroundExecutor_ != nullptr;
}
private:
friend class UIManagerBinding;
friend class Scheduler;
friend class SurfaceHandler;
/**
* Configure a LayoutAnimation to happen on the next commit.
* This API configures a global LayoutAnimation starting from the root node.
*/
void configureNextLayoutAnimation(
jsi::Runtime& runtime,
const RawValue& config,
const jsi::Value& successCallback,
const jsi::Value& failureCallback) const;
SharedComponentDescriptorRegistry componentDescriptorRegistry_;
UIManagerDelegate* delegate_{};
UIManagerAnimationDelegate* animationDelegate_{nullptr};
const RuntimeExecutor runtimeExecutor_{};
ShadowTreeRegistry shadowTreeRegistry_{};
const BackgroundExecutor backgroundExecutor_{};
ContextContainer::Shared contextContainer_;
mutable std::shared_mutex commitHookMutex_;
mutable std::vector<UIManagerCommitHook*> commitHooks_;
mutable std::shared_mutex mountHookMutex_;
mutable std::vector<UIManagerMountHook*> mountHooks_;
std::unique_ptr<LeakChecker> leakChecker_;
};
} // namespace facebook::react

View File

@@ -0,0 +1,50 @@
/*
* 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 <jsi/jsi.h>
#include <react/renderer/componentregistry/ComponentDescriptorFactory.h>
#include <react/renderer/core/RawValue.h>
namespace facebook::react {
class UIManagerAnimationDelegate {
public:
virtual ~UIManagerAnimationDelegate() = default;
/*
* Configure a LayoutAnimation.
* TODO: need SurfaceId here
*/
virtual void uiManagerDidConfigureNextLayoutAnimation(
jsi::Runtime& runtime,
const RawValue& config,
const jsi::Value& successCallback,
const jsi::Value& failureCallback) const = 0;
/**
* Set ComponentDescriptor registry.
*
* @param componentDescriptorRegistry
*/
virtual void setComponentDescriptorRegistry(
const SharedComponentDescriptorRegistry& componentDescriptorRegistry) = 0;
/**
* Only needed on Android to drive animations.
*/
virtual bool shouldAnimateFrame() const = 0;
/**
* Drop any animations for a given surface.
*/
virtual void stopSurface(SurfaceId surfaceId) = 0;
};
} // namespace facebook::react

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
/*
* 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 <folly/dynamic.h>
#include <jsi/jsi.h>
#include <react/renderer/core/RawValue.h>
#include <react/renderer/uimanager/PointerEventsProcessor.h>
#include <react/renderer/uimanager/UIManager.h>
#include <react/renderer/uimanager/primitives.h>
namespace facebook::react {
/*
* Exposes UIManager to JavaScript realm.
*/
class UIManagerBinding : public jsi::HostObject {
public:
/*
* Installs UIManagerBinding into JavaScript runtime if needed.
* Creates and sets `UIManagerBinding` into the global namespace.
* Thread synchronization must be enforced externally.
*/
static void createAndInstallIfNeeded(
jsi::Runtime& runtime,
const std::shared_ptr<UIManager>& uiManager);
/*
* Returns a pointer to UIManagerBinding previously installed into a runtime.
* Thread synchronization must be enforced externally.
*/
static std::shared_ptr<UIManagerBinding> getBinding(jsi::Runtime& runtime);
UIManagerBinding(std::shared_ptr<UIManager> uiManager);
~UIManagerBinding() override;
jsi::Value getInspectorDataForInstance(
jsi::Runtime& runtime,
const EventEmitter& eventEmitter) const;
/*
* Delivers raw event data to JavaScript.
* Thread synchronization must be enforced externally.
*/
void dispatchEvent(
jsi::Runtime& runtime,
const EventTarget* eventTarget,
const std::string& type,
ReactEventPriority priority,
const EventPayload& payload) const;
/*
* Invalidates the binding and underlying UIManager.
* Allows to save some resources and prevents UIManager's delegate to be
* called.
* Calling public methods of this class after calling this method is UB.
* Can be called on any thread.
*/
void invalidate() const;
/*
* `jsi::HostObject` specific overloads.
*/
jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& name) override;
UIManager& getUIManager();
private:
/*
* Internal method that sends the event to JS. Should only be called from
* UIManagerBinding::dispatchEvent.
*/
void dispatchEventToJS(
jsi::Runtime& runtime,
const EventTarget* eventTarget,
const std::string& type,
ReactEventPriority priority,
const EventPayload& payload) const;
std::shared_ptr<UIManager> uiManager_;
std::unique_ptr<jsi::Function> eventHandler_;
mutable PointerEventsProcessor pointerEventsProcessor_;
mutable ReactEventPriority currentEventPriority_;
};
} // namespace facebook::react

View File

@@ -0,0 +1,42 @@
/*
* 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 <react/renderer/components/root/RootShadowNode.h>
namespace facebook::react {
class ShadowTree;
class UIManager;
/*
* Implementing a commit hook allows to observe and alter Shadow Tree commits.
*/
class UIManagerCommitHook {
public:
/*
* Called right after the commit hook is registered or unregistered.
*/
virtual void commitHookWasRegistered(const UIManager& uiManager) noexcept = 0;
virtual void commitHookWasUnregistered(
const UIManager& uiManager) noexcept = 0;
/*
* Called right before a `ShadowTree` commits a new tree.
* The semantic of the method corresponds to a method of the same name
* from `ShadowTreeDelegate`.
*/
virtual RootShadowNode::Unshared shadowTreeWillCommit(
const ShadowTree& shadowTree,
const RootShadowNode::Shared& oldRootShadowNode,
const RootShadowNode::Unshared& newRootShadowNode) noexcept = 0;
virtual ~UIManagerCommitHook() noexcept = default;
};
} // namespace facebook::react

View File

@@ -0,0 +1,64 @@
/*
* 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 <react/renderer/core/ReactPrimitives.h>
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/mounting/MountingCoordinator.h>
namespace facebook::react {
/*
* Abstract class for UIManager's delegate.
*/
class UIManagerDelegate {
public:
/*
* Called right after a new/updated Shadow Node tree is constructed.
* For this moment the tree is already laid out and sealed.
*/
virtual void uiManagerDidFinishTransaction(
MountingCoordinator::Shared mountingCoordinator,
bool mountSynchronously) = 0;
/*
* Called each time when UIManager constructs a new Shadow Node. Receiver
* might use this to optimistically allocate a new native view
* instances.
*/
virtual void uiManagerDidCreateShadowNode(const ShadowNode& shadowNode) = 0;
/*
* Called when UIManager wants to dispatch a command to the mounting layer.
*/
virtual void uiManagerDidDispatchCommand(
const ShadowNode::Shared& shadowNode,
const std::string& commandName,
const folly::dynamic& args) = 0;
/*
* Called when UIManager wants to dispatch some accessibility event
* to the mounting layer. eventType is platform-specific and not all
* platforms will necessarily implement the same set of events.
*/
virtual void uiManagerDidSendAccessibilityEvent(
const ShadowNode::Shared& shadowNode,
const std::string& eventType) = 0;
/*
* Set JS responder for a view.
*/
virtual void uiManagerDidSetIsJSResponder(
const ShadowNode::Shared& shadowNode,
bool isJSResponder,
bool blockNativeResponder) = 0;
virtual ~UIManagerDelegate() noexcept = default;
};
} // namespace facebook::react

View File

@@ -0,0 +1,34 @@
/*
* 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 <react/renderer/components/root/RootShadowNode.h>
#include "UIManager.h"
namespace facebook::react {
class ShadowTree;
class UIManager;
/*
* Implementing a mount hook allows to observe Shadow Trees being mounted in
* the host platform.
*/
class UIManagerMountHook {
public:
/*
* Called right after a `ShadowTree` is mounted in the host platform.
*/
virtual void shadowTreeDidMount(
const RootShadowNode::Shared& rootShadowNode,
double mountTime) noexcept = 0;
virtual ~UIManagerMountHook() noexcept = default;
};
} // namespace facebook::react

View File

@@ -0,0 +1,79 @@
/*
* 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 "bindingUtils.h"
#include <glog/logging.h>
#include <react/debug/react_native_assert.h>
namespace facebook::react {
static jsi::Value getModule(
jsi::Runtime& runtime,
const std::string& moduleName) {
auto batchedBridge =
runtime.global().getPropertyAsObject(runtime, "__fbBatchedBridge");
auto getCallableModule =
batchedBridge.getPropertyAsFunction(runtime, "getCallableModule");
auto moduleAsValue = getCallableModule.callWithThis(
runtime,
batchedBridge,
{jsi::String::createFromUtf8(runtime, moduleName)});
if (!moduleAsValue.isObject()) {
LOG(ERROR) << "getModule of " << moduleName << " is not an object";
}
react_native_assert(moduleAsValue.isObject());
return moduleAsValue;
}
static bool checkBatchedBridgeIsActive(jsi::Runtime& runtime) {
if (!runtime.global().hasProperty(runtime, "__fbBatchedBridge")) {
LOG(ERROR)
<< "getPropertyAsObject: property '__fbBatchedBridge' is undefined, expected an Object";
return false;
}
return true;
}
static bool checkGetCallableModuleIsActive(jsi::Runtime& runtime) {
if (!checkBatchedBridgeIsActive(runtime)) {
return false;
}
auto batchedBridge =
runtime.global().getPropertyAsObject(runtime, "__fbBatchedBridge");
if (!batchedBridge.hasProperty(runtime, "getCallableModule")) {
LOG(ERROR)
<< "getPropertyAsFunction: function 'getCallableModule' is undefined, expected a Function";
return false;
}
return true;
}
jsi::Value callMethodOfModule(
jsi::Runtime& runtime,
const std::string& moduleName,
const std::string& methodName,
std::initializer_list<jsi::Value> args) {
if (checkGetCallableModuleIsActive(runtime)) {
auto module = getModule(runtime, moduleName);
if (module.isObject()) {
jsi::Object object = module.asObject(runtime);
react_native_assert(object.hasProperty(runtime, methodName.c_str()));
if (object.hasProperty(runtime, methodName.c_str())) {
auto method = object.getPropertyAsFunction(runtime, methodName.c_str());
return method.callWithThis(runtime, object, args);
} else {
LOG(ERROR) << "getPropertyAsFunction: property '" << methodName
<< "' is undefined, expected a Function";
}
}
}
return jsi::Value::undefined();
}
} // namespace facebook::react

View File

@@ -0,0 +1,20 @@
/*
* 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 <jsi/jsi.h>
namespace facebook::react {
jsi::Value callMethodOfModule(
jsi::Runtime& runtime,
const std::string& moduleName,
const std::string& methodName,
std::initializer_list<jsi::Value> args);
} // namespace facebook::react

View File

@@ -0,0 +1,239 @@
/*
* 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 <folly/dynamic.h>
#include <jsi/JSIDynamic.h>
#include <jsi/jsi.h>
#include <react/debug/react_native_assert.h>
#include <react/renderer/components/text/RawTextShadowNode.h>
#include <react/renderer/core/LayoutMetrics.h>
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/graphics/Rect.h>
namespace facebook::react {
using BackgroundExecutor =
std::function<void(std::function<void()>&& callback)>;
struct ShadowNodeListWrapper : public jsi::NativeState {
ShadowNodeListWrapper(ShadowNode::UnsharedListOfShared shadowNodeList)
: shadowNodeList(std::move(shadowNodeList)) {}
// The below method needs to be implemented out-of-line in order for the class
// to have at least one "key function" (see
// https://itanium-cxx-abi.github.io/cxx-abi/abi.html#vague-vtable)
~ShadowNodeListWrapper() override;
ShadowNode::UnsharedListOfShared shadowNodeList;
};
inline static ShadowNode::Shared shadowNodeFromValue(
jsi::Runtime& runtime,
const jsi::Value& value) {
if (value.isNull()) {
return nullptr;
}
return value.getObject(runtime).getNativeState<ShadowNode>(runtime);
}
inline static jsi::Value valueFromShadowNode(
jsi::Runtime& runtime,
ShadowNode::Shared shadowNode) {
jsi::Object obj(runtime);
// Need to const_cast since JSI only allows non-const pointees
obj.setNativeState(
runtime, std::const_pointer_cast<ShadowNode>(std::move(shadowNode)));
return obj;
}
// TODO: once we no longer need to mutate the return value (appendChildToSet)
// make this a SharedListOfShared
inline static ShadowNode::UnsharedListOfShared shadowNodeListFromValue(
jsi::Runtime& runtime,
const jsi::Value& value) {
// TODO: cleanup when passChildrenWhenCloningPersistedNodes is rolled out
jsi::Object object = value.asObject(runtime);
if (object.isArray(runtime)) {
auto jsArray = std::move(object).asArray(runtime);
size_t jsArrayLen = jsArray.length(runtime);
if (jsArrayLen > 0) {
auto shadowNodeArray = std::make_shared<ShadowNode::ListOfShared>();
shadowNodeArray->reserve(jsArrayLen);
for (size_t i = 0; i < jsArrayLen; i++) {
shadowNodeArray->push_back(
shadowNodeFromValue(runtime, jsArray.getValueAtIndex(runtime, i)));
}
return shadowNodeArray;
} else {
// TODO: return ShadowNode::emptySharedShadowNodeSharedList()
return std::make_shared<ShadowNode::ListOfShared>(
ShadowNode::ListOfShared({}));
;
}
} else {
return object.getNativeState<ShadowNodeListWrapper>(runtime)
->shadowNodeList;
}
}
inline static jsi::Value valueFromShadowNodeList(
jsi::Runtime& runtime,
ShadowNode::UnsharedListOfShared shadowNodeList) {
auto wrapper =
std::make_shared<ShadowNodeListWrapper>(std::move(shadowNodeList));
// Use the wrapper for NativeState too, otherwise we can't implement
// the marker interface. Could be simplified to a simple struct wrapper.
jsi::Object obj(runtime);
obj.setNativeState(runtime, std::move(wrapper));
return obj;
}
inline static ShadowNode::UnsharedListOfShared shadowNodeListFromWeakList(
const ShadowNode::UnsharedListOfWeak& weakShadowNodeList) {
auto result = std::make_shared<ShadowNode::ListOfShared>();
for (const auto& weakShadowNode : *weakShadowNodeList) {
auto sharedShadowNode = weakShadowNode.lock();
if (!sharedShadowNode) {
return nullptr;
}
result->push_back(sharedShadowNode);
}
return result;
}
inline static ShadowNode::UnsharedListOfWeak weakShadowNodeListFromValue(
jsi::Runtime& runtime,
const jsi::Value& value) {
auto shadowNodeList = shadowNodeListFromValue(runtime, value);
auto weakShadowNodeList = std::make_shared<ShadowNode::ListOfWeak>();
for (const auto& shadowNode : *shadowNodeList) {
weakShadowNodeList->push_back(shadowNode);
}
return weakShadowNodeList;
}
inline static Tag tagFromValue(const jsi::Value& value) {
return (Tag)value.getNumber();
}
inline static InstanceHandle::Shared instanceHandleFromValue(
jsi::Runtime& runtime,
const jsi::Value& instanceHandleValue,
const jsi::Value& tagValue) {
react_native_assert(!instanceHandleValue.isNull());
if (instanceHandleValue.isNull()) {
return nullptr;
}
return std::make_shared<InstanceHandle>(
runtime, instanceHandleValue, tagFromValue(tagValue));
}
inline static SurfaceId surfaceIdFromValue(
jsi::Runtime& runtime,
const jsi::Value& value) {
return (SurfaceId)value.getNumber();
}
inline static int displayModeToInt(const DisplayMode value) {
// the result of this method should be in sync with
// Libraries/ReactNative/DisplayMode.js
switch (value) {
case DisplayMode::Visible:
return 1;
case DisplayMode::Suspended:
return 2;
case DisplayMode::Hidden:
return 3;
}
}
inline static std::string stringFromValue(
jsi::Runtime& runtime,
const jsi::Value& value) {
return value.getString(runtime).utf8(runtime);
}
inline static folly::dynamic commandArgsFromValue(
jsi::Runtime& runtime,
const jsi::Value& value) {
return jsi::dynamicFromValue(runtime, value);
}
inline static jsi::Value 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<jsi::Value> nonNullInstanceHandles;
nonNullInstanceHandles.reserve(nodes.size());
for (const auto& shadowNode : nodes) {
auto instanceHandle = (*shadowNode).getInstanceHandle(runtime);
if (!instanceHandle.isNull()) {
nonNullInstanceHandles.push_back(std::move(instanceHandle));
}
}
auto result = jsi::Array(runtime, nonNullInstanceHandles.size());
for (size_t i = 0; i < nonNullInstanceHandles.size(); i++) {
result.setValueAtIndex(runtime, i, nonNullInstanceHandles[i]);
}
return result;
}
inline static void getTextContentInShadowNode(
const ShadowNode& shadowNode,
std::string& result) {
auto rawTextShadowNode = dynamic_cast<const RawTextShadowNode*>(&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

View File

@@ -0,0 +1,17 @@
/*
* 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 <memory>
#include <gtest/gtest.h>
#include <react/renderer/uimanager/UIManager.h>
using namespace facebook::react;
TEST(UIManagerTest, testSomething) {
// TODO
}

View File

@@ -0,0 +1,401 @@
/*
* 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 <gtest/gtest.h>
#include <jsi/jsi.h>
#include <react/renderer/element/Element.h>
#include <react/renderer/element/testUtils.h>
#include <react/renderer/uimanager/PointerEventsProcessor.h>
#include <react/renderer/uimanager/PointerHoverTracker.h>
#include <react/renderer/uimanager/UIManager.h>
namespace facebook::react {
struct PointerEventTestLogEntry {
Tag tag;
std::string eventName;
PointerEvent payload;
};
using EventLog = std::vector<PointerEventTestLogEntry>;
static inline void listenToAllPointerEvents(ViewProps& props) {
props.events[ViewEvents::Offset::PointerDown] = true;
props.events[ViewEvents::Offset::PointerMove] = true;
props.events[ViewEvents::Offset::PointerUp] = true;
props.events[ViewEvents::Offset::PointerEnter] = true;
props.events[ViewEvents::Offset::PointerLeave] = true;
props.events[ViewEvents::Offset::PointerOver] = true;
props.events[ViewEvents::Offset::PointerOut] = true;
}
class PointerEventsProcessorTest : public ::testing::Test {
public:
PointerEventsProcessorTest() {
surfaceId_ = 0;
auto contextContainer = std::make_shared<ContextContainer>();
ComponentDescriptorProviderRegistry componentDescriptorProviderRegistry{};
auto eventDispatcher = EventDispatcher::Shared{};
auto componentDescriptorRegistry =
componentDescriptorProviderRegistry.createComponentDescriptorRegistry(
ComponentDescriptorParameters{
eventDispatcher, std::move(contextContainer), nullptr});
componentDescriptorProviderRegistry.add(
concreteComponentDescriptorProvider<RootComponentDescriptor>());
componentDescriptorProviderRegistry.add(
concreteComponentDescriptorProvider<ViewComponentDescriptor>());
auto builder = ComponentBuilder{componentDescriptorRegistry};
// Set up UIManager (with no-op executors since we don't need them for
// tests)
RuntimeExecutor runtimeExecutor =
[](std::function<void(facebook::jsi::Runtime & runtime)>&& callback) {};
BackgroundExecutor backgroundExecutor =
[](std::function<void()>&& callback) {};
uiManager_ = std::make_unique<UIManager>(
runtimeExecutor, backgroundExecutor, contextContainer);
uiManager_->setComponentDescriptorRegistry(componentDescriptorRegistry);
// Create a hierarchy of nodes
/*
* Test Hierarchy:
* ┌────────────────────┐
* │ROOT │
* │ ┌───────┬──────┐ │
* │ │A │B │ │
* │ │ ┌────┼────┐ │ │
* │ │ │AA │BB │ │ │
* │ │ │ │ │ │ │
* │ │ └────┼────┘ │ │
* │ └───────┴──────┘ │
* └────────────────────┘
*/
// clang-format off
auto elementRoot =
Element<RootShadowNode>()
.tag(1)
.surfaceId(surfaceId_)
.reference(rootNode_)
.props([] {
auto sharedProps = std::make_shared<RootProps>();
auto &props = *sharedProps;
listenToAllPointerEvents(props);
props.layoutConstraints = LayoutConstraints{{0,0}, {500, 500}};
auto &yogaStyle = props.yogaStyle;
yogaStyle.setDimension(yoga::Dimension::Width, yoga::value::points(400));
yogaStyle.setDimension(yoga::Dimension::Height, yoga::value::points(400));
yogaStyle.setDisplay(yoga::Display::Flex);
yogaStyle.setFlexDirection(yoga::FlexDirection::Row);
yogaStyle.setAlignItems(yoga::Align::Center);
yogaStyle.setJustifyContent(yoga::Justify::Center);
return sharedProps;
})
.children({
Element<ViewShadowNode>()
.tag(2)
.surfaceId(surfaceId_)
.reference(nodeA_)
.props([] {
auto sharedProps = std::make_shared<ViewShadowNodeProps>();
auto &props = *sharedProps;
listenToAllPointerEvents(props);
auto &yogaStyle = props.yogaStyle;
yogaStyle.setDisplay(yoga::Display::Flex);
yogaStyle.setFlexDirection(yoga::FlexDirection::Column);
yogaStyle.setAlignItems(yoga::Align::FlexEnd);
yogaStyle.setJustifyContent(yoga::Justify::Center);
yogaStyle.setDimension(yoga::Dimension::Width, yoga::value::points(150));
yogaStyle.setDimension(yoga::Dimension::Height, yoga::value::points(300));
return sharedProps;
})
.children({
Element<ViewShadowNode>()
.tag(3)
.surfaceId(surfaceId_)
.reference(nodeAA_)
.props([] {
auto sharedProps = std::make_shared<ViewShadowNodeProps>();
auto &props = *sharedProps;
listenToAllPointerEvents(props);
auto &yogaStyle = props.yogaStyle;
yogaStyle.setDimension(yoga::Dimension::Width, yoga::value::points(100));
yogaStyle.setDimension(yoga::Dimension::Height, yoga::value::points(200));
return sharedProps;
})
}),
Element<ViewShadowNode>()
.tag(4)
.surfaceId(surfaceId_)
.reference(nodeB_)
.props([] {
auto sharedProps = std::make_shared<ViewShadowNodeProps>();
auto &props = *sharedProps;
listenToAllPointerEvents(props);
auto &yogaStyle = props.yogaStyle;
yogaStyle.setDisplay(yoga::Display::Flex);
yogaStyle.setFlexDirection(yoga::FlexDirection::Column);
yogaStyle.setAlignItems(yoga::Align::FlexStart);
yogaStyle.setJustifyContent(yoga::Justify::Center);
yogaStyle.setDimension(yoga::Dimension::Width, yoga::value::points(150));
yogaStyle.setDimension(yoga::Dimension::Height, yoga::value::points(300));
return sharedProps;
})
.children({
Element<ViewShadowNode>()
.tag(5)
.surfaceId(surfaceId_)
.reference(nodeBB_)
.props([] {
auto sharedProps = std::make_shared<ViewShadowNodeProps>();
auto &props = *sharedProps;
listenToAllPointerEvents(props);
auto &yogaStyle = props.yogaStyle;
yogaStyle.setDimension(yoga::Dimension::Width, yoga::value::points(100));
yogaStyle.setDimension(yoga::Dimension::Height, yoga::value::points(200));
return sharedProps;
})
})
})
.finalize([](RootShadowNode &shadowNode) {
shadowNode.layoutIfNeeded();
shadowNode.sealRecursive();
});
// clang-format on
// Build the node heirarchy
builder.build(elementRoot);
// Initialize shadow tree
auto layoutConstraints = LayoutConstraints{};
auto layoutContext = LayoutContext{};
auto shadowTree = std::make_unique<ShadowTree>(
surfaceId_,
layoutConstraints,
layoutContext,
*uiManager_,
*contextContainer);
shadowTree->commit(
[this](const RootShadowNode& oldRootShadowNode) {
return std::static_pointer_cast<RootShadowNode>(this->rootNode_);
},
{true});
// Start the surface in UIManager
uiManager_->startSurface(
std::move(shadowTree),
"test",
folly::dynamic::object,
DisplayMode::Visible);
}
void TearDown() override {
uiManager_->stopSurface(surfaceId_);
}
EventLog dispatchPointerEvent(
const ShadowNode::Shared& target,
std::string eventName,
PointerEvent eventPayload) {
EventLog eventLog;
auto dispatchCallback = [&eventLog](
const ShadowNode& targetNode,
const std::string& type,
ReactEventPriority priority,
const EventPayload& eventPayload) {
eventLog.push_back({
.tag = targetNode.getTag(),
.eventName = type,
.payload = static_cast<const PointerEvent&>(eventPayload),
});
};
processor_.interceptPointerEvent(
target,
eventName,
ReactEventPriority::Default,
eventPayload,
dispatchCallback,
*uiManager_);
return eventLog;
}
SurfaceId surfaceId_;
std::shared_ptr<RootShadowNode> rootNode_;
std::shared_ptr<ViewShadowNode> nodeA_;
std::shared_ptr<ViewShadowNode> nodeAA_;
std::shared_ptr<ViewShadowNode> nodeB_;
std::shared_ptr<ViewShadowNode> nodeBB_;
PointerEventsProcessor processor_;
std::unique_ptr<UIManager> uiManager_;
std::unique_ptr<jsi::Runtime> runtime_;
};
TEST_F(PointerEventsProcessorTest, moveAcross) {
auto eventPayload = PointerEvent{};
eventPayload.pointerId = 1;
// First move event inside nodeAA
auto firstMoveLog =
dispatchPointerEvent(nodeAA_, "topPointerMove", eventPayload);
EXPECT_EQ(firstMoveLog.size(), 5);
EXPECT_EQ(firstMoveLog[0].tag, nodeAA_->getTag());
EXPECT_EQ(firstMoveLog[0].eventName, "topPointerOver");
EXPECT_EQ(firstMoveLog[1].tag, rootNode_->getTag());
EXPECT_EQ(firstMoveLog[1].eventName, "topPointerEnter");
EXPECT_EQ(firstMoveLog[2].tag, nodeA_->getTag());
EXPECT_EQ(firstMoveLog[2].eventName, "topPointerEnter");
EXPECT_EQ(firstMoveLog[3].tag, nodeAA_->getTag());
EXPECT_EQ(firstMoveLog[3].eventName, "topPointerEnter");
EXPECT_EQ(firstMoveLog[4].tag, nodeAA_->getTag());
EXPECT_EQ(firstMoveLog[4].eventName, "topPointerMove");
// Second move event inside nodeBB
auto secondMoveLog =
dispatchPointerEvent(nodeBB_, "topPointerMove", eventPayload);
EXPECT_EQ(secondMoveLog.size(), 7);
EXPECT_EQ(secondMoveLog[0].tag, nodeAA_->getTag());
EXPECT_EQ(secondMoveLog[0].eventName, "topPointerOut");
EXPECT_EQ(secondMoveLog[1].tag, nodeAA_->getTag());
EXPECT_EQ(secondMoveLog[1].eventName, "topPointerLeave");
EXPECT_EQ(secondMoveLog[2].tag, nodeA_->getTag());
EXPECT_EQ(secondMoveLog[2].eventName, "topPointerLeave");
EXPECT_EQ(secondMoveLog[3].tag, nodeBB_->getTag());
EXPECT_EQ(secondMoveLog[3].eventName, "topPointerOver");
EXPECT_EQ(secondMoveLog[4].tag, nodeB_->getTag());
EXPECT_EQ(secondMoveLog[4].eventName, "topPointerEnter");
EXPECT_EQ(secondMoveLog[5].tag, nodeBB_->getTag());
EXPECT_EQ(secondMoveLog[5].eventName, "topPointerEnter");
EXPECT_EQ(secondMoveLog[6].tag, nodeBB_->getTag());
EXPECT_EQ(secondMoveLog[6].eventName, "topPointerMove");
// Third move event also inside nodeBB (should be no derivative events
// emitted)
auto thirdMoveLog =
dispatchPointerEvent(nodeBB_, "topPointerMove", eventPayload);
EXPECT_EQ(thirdMoveLog.size(), 1);
EXPECT_EQ(thirdMoveLog[0].tag, nodeBB_->getTag());
EXPECT_EQ(thirdMoveLog[0].eventName, "topPointerMove");
// Last event emulates an event reporting that the pointer has left the root
// view
auto leavingMoveLog =
dispatchPointerEvent(rootNode_, "topPointerLeave", eventPayload);
EXPECT_EQ(leavingMoveLog.size(), 4);
EXPECT_EQ(leavingMoveLog[0].tag, nodeBB_->getTag());
EXPECT_EQ(leavingMoveLog[0].eventName, "topPointerOut");
EXPECT_EQ(leavingMoveLog[1].tag, nodeBB_->getTag());
EXPECT_EQ(leavingMoveLog[1].eventName, "topPointerLeave");
EXPECT_EQ(leavingMoveLog[2].tag, nodeB_->getTag());
EXPECT_EQ(leavingMoveLog[2].eventName, "topPointerLeave");
EXPECT_EQ(leavingMoveLog[3].tag, rootNode_->getTag());
EXPECT_EQ(leavingMoveLog[3].eventName, "topPointerLeave");
}
TEST_F(PointerEventsProcessorTest, directPress) {
auto eventPayload = PointerEvent{};
eventPayload.pointerId = 1;
// Emulate down event from the platform onto nodeA
auto downLog = dispatchPointerEvent(nodeA_, "topPointerDown", eventPayload);
EXPECT_EQ(downLog.size(), 4);
EXPECT_EQ(downLog[0].tag, nodeA_->getTag());
EXPECT_EQ(downLog[0].eventName, "topPointerOver");
EXPECT_EQ(downLog[1].tag, rootNode_->getTag());
EXPECT_EQ(downLog[1].eventName, "topPointerEnter");
EXPECT_EQ(downLog[2].tag, nodeA_->getTag());
EXPECT_EQ(downLog[2].eventName, "topPointerEnter");
EXPECT_EQ(downLog[3].tag, nodeA_->getTag());
EXPECT_EQ(downLog[3].eventName, "topPointerDown");
// Emulate an up event on nodeA
auto upLog = dispatchPointerEvent(nodeA_, "topPointerUp", eventPayload);
EXPECT_EQ(upLog.size(), 4);
EXPECT_EQ(upLog[0].tag, nodeA_->getTag());
EXPECT_EQ(upLog[0].eventName, "topPointerUp");
EXPECT_EQ(upLog[1].tag, nodeA_->getTag());
EXPECT_EQ(upLog[1].eventName, "topPointerOut");
EXPECT_EQ(upLog[2].tag, nodeA_->getTag());
EXPECT_EQ(upLog[2].eventName, "topPointerLeave");
EXPECT_EQ(upLog[3].tag, rootNode_->getTag());
EXPECT_EQ(upLog[3].eventName, "topPointerLeave");
}
TEST_F(PointerEventsProcessorTest, indirectPress) {
auto eventPayload = PointerEvent{};
eventPayload.pointerId = 1;
// Emulate a move event before the press event sequence
auto moveLog = dispatchPointerEvent(nodeA_, "topPointerMove", eventPayload);
EXPECT_EQ(moveLog.size(), 4);
EXPECT_EQ(moveLog[0].tag, nodeA_->getTag());
EXPECT_EQ(moveLog[0].eventName, "topPointerOver");
EXPECT_EQ(moveLog[1].tag, rootNode_->getTag());
EXPECT_EQ(moveLog[1].eventName, "topPointerEnter");
EXPECT_EQ(moveLog[2].tag, nodeA_->getTag());
EXPECT_EQ(moveLog[2].eventName, "topPointerEnter");
EXPECT_EQ(moveLog[3].tag, nodeA_->getTag());
EXPECT_EQ(moveLog[3].eventName, "topPointerMove");
// Emulate a down event from the platform onto nodeA
auto downLog = dispatchPointerEvent(nodeA_, "topPointerDown", eventPayload);
EXPECT_EQ(downLog.size(), 1);
EXPECT_EQ(downLog[0].tag, nodeA_->getTag());
EXPECT_EQ(downLog[0].eventName, "topPointerDown");
// Emulate an up event on nodeA
auto upLog = dispatchPointerEvent(nodeA_, "topPointerUp", eventPayload);
EXPECT_EQ(upLog.size(), 1);
EXPECT_EQ(upLog[0].tag, nodeA_->getTag());
EXPECT_EQ(upLog[0].eventName, "topPointerUp");
}
} // namespace facebook::react