first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
# 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)
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
file(GLOB react_renderer_observers_intersection_SRC CONFIGURE_DEPENDS *.cpp)
add_library(react_renderer_observers_intersection OBJECT ${react_renderer_observers_intersection_SRC})
target_include_directories(react_renderer_observers_intersection PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(react_renderer_observers_intersection
react_cxxreact
react_bridging
react_debug
react_renderer_core
react_renderer_graphics
react_renderer_mounting
react_renderer_runtimescheduler
react_renderer_uimanager
rrc_view
)
target_compile_reactnative_options(react_renderer_observers_intersection PRIVATE)
target_compile_options(react_renderer_observers_intersection PRIVATE -Wpedantic)

View File

@@ -0,0 +1,390 @@
/*
* 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 "IntersectionObserver.h"
#include <react/debug/react_native_assert.h>
#include <react/renderer/core/LayoutMetrics.h>
#include <react/renderer/core/LayoutableShadowNode.h>
#include <react/renderer/core/ShadowNodeFamily.h>
#include <react/renderer/css/CSSLength.h>
#include <react/renderer/css/CSSPercentage.h>
#include <react/renderer/css/CSSValueParser.h>
#include <react/renderer/graphics/RectangleEdges.h>
#include <utility>
namespace facebook::react {
// Parse a normalized rootMargin string that's always in the format:
// "top right bottom left" where each value is either "Npx" or "N%"
// The string is already validated and normalized by JS.
// Returns a vector of 4 MarginValue structures (top, right, bottom, left).
// Returns an empty vector if parsing fails.
std::vector<MarginValue> parseNormalizedRootMargin(
const std::string& marginStr) {
// If unset or set to default, return empty so we don't apply any margins
if (marginStr.empty() || marginStr == "0px 0px 0px 0px") {
return {};
}
std::vector<MarginValue> values;
CSSSyntaxParser syntaxParser(marginStr);
// Parse exactly 4 space-separated values
while (!syntaxParser.isFinished() && values.size() < 4) {
syntaxParser.consumeWhitespace();
if (syntaxParser.isFinished()) {
break;
}
auto parsed = parseNextCSSValue<CSSLength, CSSPercentage>(syntaxParser);
if (std::holds_alternative<CSSLength>(parsed)) {
auto length = std::get<CSSLength>(parsed);
// Only support px units for rootMargin (per W3C spec)
if (length.unit != CSSLengthUnit::Px) {
return {};
}
values.push_back({length.value, false});
} else if (std::holds_alternative<CSSPercentage>(parsed)) {
auto percentage = std::get<CSSPercentage>(parsed);
values.push_back({percentage.value, true});
} else {
// Invalid token
return {};
}
}
// Should have exactly 4 values since JS normalizes to this format
if (values.size() != 4) {
return {};
}
return values;
}
namespace {
// Convert margin values to actual pixel values based on root rect.
// Per W3C spec: top/bottom percentages use height, left/right use width.
EdgeInsets calculateRootMarginInsets(
const std::vector<MarginValue>& margins,
const Rect& rootRect) {
Float top = margins[0].isPercentage
? (margins[0].value / 100.0f) * rootRect.size.height
: margins[0].value;
Float right = margins[1].isPercentage
? (margins[1].value / 100.0f) * rootRect.size.width
: margins[1].value;
Float bottom = margins[2].isPercentage
? (margins[2].value / 100.0f) * rootRect.size.height
: margins[2].value;
Float left = margins[3].isPercentage
? (margins[3].value / 100.0f) * rootRect.size.width
: margins[3].value;
return EdgeInsets{left, top, right, bottom};
}
} // namespace
IntersectionObserver::IntersectionObserver(
IntersectionObserverObserverId intersectionObserverId,
std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily,
ShadowNodeFamily::Shared targetShadowNodeFamily,
std::vector<Float> thresholds,
std::optional<std::vector<Float>> rootThresholds,
std::vector<MarginValue> rootMargins)
: intersectionObserverId_(intersectionObserverId),
observationRootShadowNodeFamily_(
std::move(observationRootShadowNodeFamily)),
targetShadowNodeFamily_(std::move(targetShadowNodeFamily)),
thresholds_(std::move(thresholds)),
rootThresholds_(std::move(rootThresholds)),
rootMargins_(std::move(rootMargins)) {}
static std::shared_ptr<const ShadowNode> getShadowNode(
const ShadowNodeFamily::AncestorList& ancestors) {
if (ancestors.empty()) {
return nullptr;
}
const auto& lastAncestor = ancestors.back();
const ShadowNode& parentNode = lastAncestor.first.get();
int childIndex = lastAncestor.second;
const std::shared_ptr<const ShadowNode>& childNode =
parentNode.getChildren().at(childIndex);
return childNode;
}
static Rect getRootNodeBoundingRect(const RootShadowNode& rootShadowNode) {
const auto layoutableRootShadowNode =
dynamic_cast<const LayoutableShadowNode*>(&rootShadowNode);
react_native_assert(
layoutableRootShadowNode != nullptr &&
"RootShadowNode instances must always inherit from LayoutableShadowNode.");
auto layoutMetrics = layoutableRootShadowNode->getLayoutMetrics();
if (layoutMetrics == EmptyLayoutMetrics ||
layoutMetrics.displayType == DisplayType::None) {
return Rect{};
}
// Apply the transform to translate the root view to its location in the
// viewport.
return layoutMetrics.frame * layoutableRootShadowNode->getTransform();
}
static Rect getBoundingRect(const ShadowNodeFamily::AncestorList& ancestors) {
auto layoutMetrics = LayoutableShadowNode::computeRelativeLayoutMetrics(
ancestors,
{/* .includeTransform = */ .includeTransform = true,
/* .includeViewportOffset = */ .includeViewportOffset = true});
return layoutMetrics == EmptyLayoutMetrics ? Rect{} : layoutMetrics.frame;
}
static Rect getClippedTargetBoundingRect(
const ShadowNodeFamily::AncestorList& targetAncestors) {
auto layoutMetrics = LayoutableShadowNode::computeRelativeLayoutMetrics(
targetAncestors,
{/* .includeTransform = */ .includeTransform = true,
/* .includeViewportOffset = */ .includeViewportOffset = true,
/* .applyParentClipping = */ .enableOverflowClipping = true});
return layoutMetrics == EmptyLayoutMetrics ? Rect{} : layoutMetrics.frame;
}
// Distinguishes between edge-adjacent vs. no intersection
static std::optional<Rect> intersectOrNull(
const Rect& rect1,
const Rect& rect2) {
auto result = Rect::intersect(rect1, rect2);
// Check if the result has zero area (could be empty or degenerate)
if (result.size.width == 0 || result.size.height == 0) {
// Check if origin is within both rectangles (touching case)
bool originInRect1 = result.origin.x >= rect1.getMinX() &&
result.origin.x <= rect1.getMaxX() &&
result.origin.y >= rect1.getMinY() &&
result.origin.y <= rect1.getMaxY();
bool originInRect2 = result.origin.x >= rect2.getMinX() &&
result.origin.x <= rect2.getMaxX() &&
result.origin.y >= rect2.getMinY() &&
result.origin.y <= rect2.getMaxY();
if (!originInRect1 || !originInRect2) {
// No actual intersection - rectangles are separated
return std::nullopt;
}
}
// Valid intersection (including degenerate edge/corner cases)
return result;
}
// Partially equivalent to
// https://w3c.github.io/IntersectionObserver/#calculate-intersection-rect-algo
static std::optional<Rect> computeIntersection(
const Rect& rootBoundingRect,
const Rect& rootMarginBoundingRect,
const Rect& targetBoundingRect,
const ShadowNodeFamily::AncestorList& targetToRootAncestors,
bool hasExplicitRoot) {
// Use intersectOrNull to properly distinguish between edge-adjacent
// (valid intersection) and separated rectangles (no intersection)
auto absoluteIntersectionRect =
intersectOrNull(rootMarginBoundingRect, targetBoundingRect);
if (!absoluteIntersectionRect.has_value()) {
return std::nullopt;
}
// Coordinates of the target after clipping the parts hidden by a parent,
// until till the root (e.g.: in scroll views, or in views with a parent with
// overflow: hidden)
auto clippedTargetFromRoot =
getClippedTargetBoundingRect(targetToRootAncestors);
// Use root origin (without rootMargins) to translate coordinates of
// clippedTarget from relative to root, to top-level coordinate system
auto clippedTargetBoundingRect = hasExplicitRoot ? Rect{
.origin= rootBoundingRect.origin + clippedTargetFromRoot.origin,
.size=clippedTargetFromRoot.size}
: clippedTargetFromRoot;
return intersectOrNull(rootMarginBoundingRect, clippedTargetBoundingRect);
}
static Float getHighestThresholdCrossed(
Float intersectionRatio,
const std::vector<Float>& thresholds) {
Float highestThreshold = -1.0f;
for (auto threshold : thresholds) {
if (intersectionRatio >= threshold) {
highestThreshold = threshold;
}
}
return highestThreshold;
}
// Partially equivalent to
// https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
std::optional<IntersectionObserverEntry>
IntersectionObserver::updateIntersectionObservation(
const RootShadowNode& rootShadowNode,
HighResTimeStamp time) {
bool hasExplicitRoot = observationRootShadowNodeFamily_.has_value();
auto rootAncestors = hasExplicitRoot
? observationRootShadowNodeFamily_.value()->getAncestors(rootShadowNode)
: ShadowNodeFamily::AncestorList{};
// Absolute coordinates of the root
auto rootBoundingRect = hasExplicitRoot
? getBoundingRect(rootAncestors)
: getRootNodeBoundingRect(rootShadowNode);
auto rootMarginBoundingRect = rootBoundingRect;
// Apply rootMargin to expand/contract the root bounding rect
if (!rootMargins_.empty()) {
// Calculate the actual insets based on current root dimensions
// (converts percentages to pixels)
auto insets = calculateRootMarginInsets(rootMargins_, rootBoundingRect);
// Use outsetBy to expand the root rect (positive values expand, negative
// contract)
rootMarginBoundingRect = outsetBy(rootBoundingRect, insets);
}
auto targetAncestors = targetShadowNodeFamily_->getAncestors(rootShadowNode);
// Absolute coordinates of the target
auto targetBoundingRect = getBoundingRect(targetAncestors);
if ((hasExplicitRoot && rootAncestors.empty()) || targetAncestors.empty()) {
// If observation root or target is not a descendant of `rootShadowNode`
return setNotIntersectingState(
rootMarginBoundingRect, targetBoundingRect, {}, time);
}
auto targetToRootAncestors = hasExplicitRoot
? targetShadowNodeFamily_->getAncestors(*getShadowNode(rootAncestors))
: targetAncestors;
auto intersection = computeIntersection(
rootBoundingRect,
rootMarginBoundingRect,
targetBoundingRect,
targetToRootAncestors,
hasExplicitRoot);
auto intersectionRect =
intersection.has_value() ? intersection.value() : Rect{};
Float targetBoundingRectArea =
targetBoundingRect.size.width * targetBoundingRect.size.height;
auto intersectionRectArea =
intersectionRect.size.width * intersectionRect.size.height;
Float intersectionRatio =
targetBoundingRectArea == 0 // prevent division by zero
? 0
: intersectionRectArea / targetBoundingRectArea;
if (!intersection.has_value()) {
return setNotIntersectingState(
rootMarginBoundingRect, targetBoundingRect, intersectionRect, time);
}
auto highestThresholdCrossed =
getHighestThresholdCrossed(intersectionRatio, thresholds_);
auto highestRootThresholdCrossed = -1.0f;
if (rootThresholds_.has_value()) {
Float rootMarginBoundingRectArea =
rootMarginBoundingRect.size.width * rootMarginBoundingRect.size.height;
Float rootThresholdIntersectionRatio = rootMarginBoundingRectArea == 0
? 0
: intersectionRectArea / rootMarginBoundingRectArea;
highestRootThresholdCrossed = getHighestThresholdCrossed(
rootThresholdIntersectionRatio, rootThresholds_.value());
}
if (highestThresholdCrossed == -1.0f &&
highestRootThresholdCrossed == -1.0f) {
return setNotIntersectingState(
rootMarginBoundingRect, targetBoundingRect, intersectionRect, time);
}
return setIntersectingState(
rootMarginBoundingRect,
targetBoundingRect,
intersectionRect,
highestThresholdCrossed,
highestRootThresholdCrossed,
time);
}
std::optional<IntersectionObserverEntry>
IntersectionObserver::updateIntersectionObservationForSurfaceUnmount(
HighResTimeStamp time) {
return setNotIntersectingState(Rect{}, Rect{}, Rect{}, time);
}
std::optional<IntersectionObserverEntry>
IntersectionObserver::setIntersectingState(
const Rect& rootBoundingRect,
const Rect& targetBoundingRect,
const Rect& intersectionRect,
Float threshold,
Float rootThreshold,
HighResTimeStamp time) {
auto newState =
IntersectionObserverState::Intersecting(threshold, rootThreshold);
if (state_ != newState) {
state_ = newState;
IntersectionObserverEntry entry{
.intersectionObserverId = intersectionObserverId_,
.shadowNodeFamily = targetShadowNodeFamily_,
.targetRect = targetBoundingRect,
.rootRect = rootBoundingRect,
.intersectionRect = intersectionRect,
.isIntersectingAboveThresholds = true,
.time = time,
};
return std::optional<IntersectionObserverEntry>{std::move(entry)};
}
return std::nullopt;
}
std::optional<IntersectionObserverEntry>
IntersectionObserver::setNotIntersectingState(
const Rect& rootBoundingRect,
const Rect& targetBoundingRect,
const Rect& intersectionRect,
HighResTimeStamp time) {
if (state_ != IntersectionObserverState::NotIntersecting()) {
state_ = IntersectionObserverState::NotIntersecting();
IntersectionObserverEntry entry{
.intersectionObserverId = intersectionObserverId_,
.shadowNodeFamily = targetShadowNodeFamily_,
.targetRect = targetBoundingRect,
.rootRect = rootBoundingRect,
.intersectionRect = intersectionRect,
.isIntersectingAboveThresholds = false,
.time = time,
};
return std::optional<IntersectionObserverEntry>(std::move(entry));
}
return std::nullopt;
}
} // namespace facebook::react

View File

@@ -0,0 +1,106 @@
/*
* 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 <react/renderer/core/ShadowNodeFamily.h>
#include <react/renderer/graphics/Float.h>
#include <react/renderer/graphics/Rect.h>
#include <memory>
#include "IntersectionObserverState.h"
namespace facebook::react {
using IntersectionObserverObserverId = int32_t;
// Structure to hold a margin value that can be either pixels or percentage
struct MarginValue {
Float value;
bool isPercentage;
};
// Parse a normalized rootMargin string that's always in the format:
// "top right bottom left" where each value is either "Npx" or "N%"
// The string is already validated and normalized by JS.
// Returns a vector of 4 MarginValue structures (top, right, bottom, left).
std::vector<MarginValue> parseNormalizedRootMargin(const std::string &marginStr);
struct IntersectionObserverEntry {
IntersectionObserverObserverId intersectionObserverId;
ShadowNodeFamily::Shared shadowNodeFamily;
Rect targetRect;
Rect rootRect;
Rect intersectionRect;
bool isIntersectingAboveThresholds;
HighResTimeStamp time;
bool sameShadowNodeFamily(const ShadowNodeFamily &otherShadowNodeFamily) const
{
return std::addressof(*shadowNodeFamily) == std::addressof(otherShadowNodeFamily);
}
};
class IntersectionObserver {
public:
IntersectionObserver(
IntersectionObserverObserverId intersectionObserverId,
std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily,
ShadowNodeFamily::Shared targetShadowNodeFamily,
std::vector<Float> thresholds,
std::optional<std::vector<Float>> rootThresholds,
std::vector<MarginValue> rootMargins);
// Partially equivalent to
// https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
std::optional<IntersectionObserverEntry> updateIntersectionObservation(
const RootShadowNode &rootShadowNode,
HighResTimeStamp time);
std::optional<IntersectionObserverEntry> updateIntersectionObservationForSurfaceUnmount(HighResTimeStamp time);
IntersectionObserverObserverId getIntersectionObserverId() const
{
return intersectionObserverId_;
}
ShadowNodeFamily::Shared getTargetShadowNodeFamily() const
{
return targetShadowNodeFamily_;
}
std::vector<Float> getThresholds() const
{
return thresholds_;
}
private:
std::optional<IntersectionObserverEntry> setIntersectingState(
const Rect &rootBoundingRect,
const Rect &targetBoundingRect,
const Rect &intersectionRect,
Float threshold,
Float rootThreshold,
HighResTimeStamp time);
std::optional<IntersectionObserverEntry> setNotIntersectingState(
const Rect &rootBoundingRect,
const Rect &targetBoundingRect,
const Rect &intersectionRect,
HighResTimeStamp time);
IntersectionObserverObserverId intersectionObserverId_;
std::optional<ShadowNodeFamily::Shared> observationRootShadowNodeFamily_;
ShadowNodeFamily::Shared targetShadowNodeFamily_;
std::vector<Float> thresholds_;
std::optional<std::vector<Float>> rootThresholds_;
// Parsed and expanded rootMargin values (top, right, bottom, left)
std::vector<MarginValue> rootMargins_;
mutable IntersectionObserverState state_ = IntersectionObserverState::Initial();
};
} // namespace facebook::react

View File

@@ -0,0 +1,336 @@
/*
* 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 "IntersectionObserverManager.h"
#include <cxxreact/JSExecutor.h>
#include <cxxreact/TraceSection.h>
#include <react/debug/react_native_assert.h>
#include <utility>
#include "IntersectionObserver.h"
namespace facebook::react {
namespace {
RootShadowNode::Shared getRootShadowNode(
SurfaceId surfaceId,
const ShadowTreeRegistry& shadowTreeRegistry,
std::unordered_map<SurfaceId, RootShadowNode::Shared>& cache) {
auto it = cache.find(surfaceId);
if (it == cache.end()) {
RootShadowNode::Shared rootShadowNode = nullptr;
shadowTreeRegistry.visit(surfaceId, [&](const ShadowTree& shadowTree) {
rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode;
});
cache.insert({surfaceId, rootShadowNode});
return rootShadowNode;
} else {
return it->second;
}
}
} // namespace
IntersectionObserverManager::IntersectionObserverManager() = default;
void IntersectionObserverManager::observe(
IntersectionObserverObserverId intersectionObserverId,
const std::optional<ShadowNodeFamily::Shared>&
observationRootShadowNodeFamily,
const ShadowNodeFamily::Shared& shadowNodeFamily,
std::vector<Float> thresholds,
std::optional<std::vector<Float>> rootThresholds,
std::optional<std::string> rootMargin,
const UIManager& /*uiManager*/) {
TraceSection s("IntersectionObserverManager::observe");
auto surfaceId = shadowNodeFamily->getSurfaceId();
// Register observer
std::unique_lock lock(observersMutex_);
auto& observers = observersBySurfaceId_[surfaceId];
// Parse rootMargin string into MarginValue structures
// Default to "0px 0px 0px 0px" if not provided
auto parsedRootMargin =
parseNormalizedRootMargin(rootMargin.value_or("0px 0px 0px 0px"));
observers.emplace_back(
std::make_unique<IntersectionObserver>(
intersectionObserverId,
observationRootShadowNodeFamily,
shadowNodeFamily,
std::move(thresholds),
std::move(rootThresholds),
std::move(parsedRootMargin)));
observersPendingInitialization_.emplace_back(observers.back().get());
}
void IntersectionObserverManager::unobserve(
IntersectionObserverObserverId intersectionObserverId,
const ShadowNodeFamily::Shared& shadowNodeFamily) {
TraceSection s("IntersectionObserverManager::unobserve");
// This doesn't need to be protected by the mutex because it is only
// accessed and modified from the JS thread.
observersPendingInitialization_.erase(
std::remove_if(
observersPendingInitialization_.begin(),
observersPendingInitialization_.end(),
[intersectionObserverId, &shadowNodeFamily](const auto& observer) {
return (
observer->getIntersectionObserverId() ==
intersectionObserverId &&
observer->getTargetShadowNodeFamily() == shadowNodeFamily);
}),
observersPendingInitialization_.end());
{
std::unique_lock lock(observersMutex_);
auto surfaceId = shadowNodeFamily->getSurfaceId();
auto observersIt = observersBySurfaceId_.find(surfaceId);
if (observersIt == observersBySurfaceId_.end()) {
return;
}
auto& observers = observersIt->second;
observers.erase(
std::remove_if(
observers.begin(),
observers.end(),
[intersectionObserverId, &shadowNodeFamily](const auto& observer) {
return observer->getIntersectionObserverId() ==
intersectionObserverId &&
observer->getTargetShadowNodeFamily() == shadowNodeFamily;
}),
observers.end());
if (observers.empty()) {
observersBySurfaceId_.erase(surfaceId);
}
}
{
std::unique_lock lock(pendingEntriesMutex_);
pendingEntries_.erase(
std::remove_if(
pendingEntries_.begin(),
pendingEntries_.end(),
[intersectionObserverId, &shadowNodeFamily](const auto& entry) {
return entry.intersectionObserverId == intersectionObserverId &&
entry.sameShadowNodeFamily(*shadowNodeFamily);
}),
pendingEntries_.end());
}
}
void IntersectionObserverManager::connect(
RuntimeScheduler& runtimeScheduler,
UIManager& uiManager,
std::function<void()> notifyIntersectionObserversCallback) {
TraceSection s("IntersectionObserverManager::connect");
notifyIntersectionObserversCallback_ =
std::move(notifyIntersectionObserversCallback);
// Fail-safe in case the caller doesn't guarantee consistency.
if (mountHookRegistered_) {
return;
}
runtimeScheduler.setIntersectionObserverDelegate(this);
uiManager.registerMountHook(*this);
shadowTreeRegistry_ = &uiManager.getShadowTreeRegistry();
mountHookRegistered_ = true;
}
void IntersectionObserverManager::disconnect(
RuntimeScheduler& runtimeScheduler,
UIManager& uiManager) {
TraceSection s("IntersectionObserverManager::disconnect");
// Fail-safe in case the caller doesn't guarantee consistency.
if (!mountHookRegistered_) {
return;
}
runtimeScheduler.setIntersectionObserverDelegate(nullptr);
uiManager.unregisterMountHook(*this);
shadowTreeRegistry_ = nullptr;
mountHookRegistered_ = false;
notifyIntersectionObserversCallback_ = nullptr;
}
std::vector<IntersectionObserverEntry>
IntersectionObserverManager::takeRecords() {
std::unique_lock lock(pendingEntriesMutex_);
notifiedIntersectionObservers_ = false;
std::vector<IntersectionObserverEntry> entries;
pendingEntries_.swap(entries);
return entries;
}
#pragma mark - RuntimeSchedulerIntersectionObserverDelegate
void IntersectionObserverManager::updateIntersectionObservations(
const std::unordered_set<SurfaceId>&
surfaceIdsWithPendingRenderingUpdates) {
// On Web, this step invoked from the Event Loop computes the intersections
// and schedules the notifications.
// Doing exactly the same in React Native would exclude the time it takes
// to process these transactions and mount the operations in the host
// platform, which would provide inaccurate timings for measuring paint time.
// Instead, we use mount hooks to compute this.
// What we can use this step for is to dispatch initial notifications for
// observers that were just set up, but for which we do not have any pending
// transactions that would trigger the mount hooks.
// In those cases it is ok to dispatch the notifications now, because the
// current state is already accurate.
if (observersPendingInitialization_.empty()) {
return;
}
TraceSection s(
"IntersectionObserverManager::updateIntersectionObservations",
"pendingObserverCount",
observersPendingInitialization_.size());
std::unordered_map<SurfaceId, RootShadowNode::Shared> rootShadowNodeCache;
for (auto observer : observersPendingInitialization_) {
auto surfaceId = observer->getTargetShadowNodeFamily()->getSurfaceId();
// If there are pending updates, we just wait for the mount hook.
if (surfaceIdsWithPendingRenderingUpdates.contains(surfaceId)) {
continue;
}
RootShadowNode::Shared rootShadowNode =
getRootShadowNode(surfaceId, *shadowTreeRegistry_, rootShadowNodeCache);
// If the surface doesn't exist for some reason, we skip initial
// notification.
if (!rootShadowNode) {
continue;
}
auto entry = observer->updateIntersectionObservation(
*rootShadowNode, HighResTimeStamp::now());
if (entry) {
{
std::unique_lock lock(pendingEntriesMutex_);
pendingEntries_.push_back(std::move(entry).value());
}
notifyObserversIfNecessary();
}
}
observersPendingInitialization_.clear();
}
#pragma mark - UIManagerMountHook
void IntersectionObserverManager::shadowTreeDidMount(
const RootShadowNode::Shared& rootShadowNode,
HighResTimeStamp time) noexcept {
TraceSection s("IntersectionObserverManager::shadowTreeDidMount");
updateIntersectionObservations(
rootShadowNode->getSurfaceId(), rootShadowNode.get(), time);
}
void IntersectionObserverManager::shadowTreeDidUnmount(
SurfaceId surfaceId,
HighResTimeStamp time) noexcept {
TraceSection s("IntersectionObserverManager::shadowTreeDidUnmount");
updateIntersectionObservations(surfaceId, nullptr, time);
}
#pragma mark - Private methods
void IntersectionObserverManager::updateIntersectionObservations(
SurfaceId surfaceId,
const RootShadowNode* rootShadowNode,
HighResTimeStamp time) {
std::vector<IntersectionObserverEntry> entries;
// Run intersection observations
{
std::shared_lock lock(observersMutex_);
auto observersIt = observersBySurfaceId_.find(surfaceId);
if (observersIt == observersBySurfaceId_.end()) {
return;
}
TraceSection s(
"IntersectionObserverManager::updateIntersectionObservations(mount)",
"observerCount",
observersIt->second.size());
auto& observers = observersIt->second;
for (auto& observer : observers) {
std::optional<IntersectionObserverEntry> entry;
if (rootShadowNode != nullptr) {
entry = observer->updateIntersectionObservation(*rootShadowNode, time);
} else {
entry = observer->updateIntersectionObservationForSurfaceUnmount(time);
}
if (entry) {
entries.push_back(std::move(entry).value());
}
}
}
{
std::unique_lock lock(pendingEntriesMutex_);
pendingEntries_.insert(
pendingEntries_.end(), entries.begin(), entries.end());
}
notifyObserversIfNecessary();
}
/**
* This method allows us to avoid scheduling multiple calls to notify observers
* in the JS thread. We schedule one and skip subsequent ones (we just append
* the entries to the pending list and wait for the scheduled task to consume
* all of them).
*/
void IntersectionObserverManager::notifyObserversIfNecessary() {
bool dispatchNotification = false;
{
std::unique_lock lock(pendingEntriesMutex_);
if (!pendingEntries_.empty() && !notifiedIntersectionObservers_) {
notifiedIntersectionObservers_ = true;
dispatchNotification = true;
}
}
if (dispatchNotification) {
notifyObservers();
}
}
void IntersectionObserverManager::notifyObservers() {
TraceSection s("IntersectionObserverManager::notifyObservers");
notifyIntersectionObserversCallback_();
}
} // namespace facebook::react

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 <react/renderer/core/ShadowNode.h>
#include <react/renderer/graphics/Float.h>
#include <react/renderer/runtimescheduler/RuntimeScheduler.h>
#include <react/renderer/runtimescheduler/RuntimeSchedulerIntersectionObserverDelegate.h>
#include <react/renderer/uimanager/UIManager.h>
#include <react/renderer/uimanager/UIManagerMountHook.h>
#include <memory>
#include <vector>
#include "IntersectionObserver.h"
namespace facebook::react {
class IntersectionObserverManager final : public UIManagerMountHook,
public RuntimeSchedulerIntersectionObserverDelegate {
public:
IntersectionObserverManager();
void observe(
IntersectionObserverObserverId intersectionObserverId,
const std::optional<ShadowNodeFamily::Shared> &observationRootShadowNode,
const ShadowNodeFamily::Shared &shadowNode,
std::vector<Float> thresholds,
std::optional<std::vector<Float>> rootThresholds,
std::optional<std::string> rootMargin,
const UIManager &uiManager);
void unobserve(IntersectionObserverObserverId intersectionObserverId, const ShadowNodeFamily::Shared &shadowNode);
void connect(
RuntimeScheduler &runtimeScheduler,
UIManager &uiManager,
std::function<void()> notifyIntersectionObserversCallback);
void disconnect(RuntimeScheduler &runtimeScheduler, UIManager &uiManager);
std::vector<IntersectionObserverEntry> takeRecords();
#pragma mark - RuntimeSchedulerIntersectionObserverDelegate
void updateIntersectionObservations(
const std::unordered_set<SurfaceId> &surfaceIdsWithPendingRenderingUpdates) override;
#pragma mark - UIManagerMountHook
void shadowTreeDidMount(const RootShadowNode::Shared &rootShadowNode, HighResTimeStamp time) noexcept override;
void shadowTreeDidUnmount(SurfaceId surfaceId, HighResTimeStamp time) noexcept override;
private:
mutable std::unordered_map<SurfaceId, std::vector<std::unique_ptr<IntersectionObserver>>> observersBySurfaceId_;
mutable std::shared_mutex observersMutex_;
// This is defined as a list of pointers to keep the ownership of the
// observers in the map.
std::vector<IntersectionObserver *> observersPendingInitialization_;
// This is only accessed from the JS thread at the end of the event loop tick,
// so it is safe to retain it as a raw pointer.
// We need to retain it here because the RuntimeScheduler does not provide
// it when calling `updateIntersectionObservations`.
const ShadowTreeRegistry *shadowTreeRegistry_{nullptr};
mutable std::function<void()> notifyIntersectionObserversCallback_;
mutable std::vector<IntersectionObserverEntry> pendingEntries_;
mutable std::mutex pendingEntriesMutex_;
mutable bool notifiedIntersectionObservers_{};
mutable bool mountHookRegistered_{};
void notifyObserversIfNecessary();
void notifyObservers();
// Equivalent to
// https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
void updateIntersectionObservations(SurfaceId surfaceId, const RootShadowNode *rootShadowNode, HighResTimeStamp time);
const IntersectionObserver &getRegisteredIntersectionObserver(
SurfaceId surfaceId,
IntersectionObserverObserverId observerId) const;
};
} // namespace facebook::react

View File

@@ -0,0 +1,63 @@
/*
* 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 "IntersectionObserverState.h"
namespace facebook::react {
IntersectionObserverState IntersectionObserverState::Initial() {
static const IntersectionObserverState state =
IntersectionObserverState(IntersectionObserverStateType::Initial);
return state;
}
IntersectionObserverState IntersectionObserverState::NotIntersecting() {
static const IntersectionObserverState state =
IntersectionObserverState(IntersectionObserverStateType::NotIntersecting);
return state;
}
IntersectionObserverState IntersectionObserverState::Intersecting(
Float threshold,
Float rootThreshold) {
return {
IntersectionObserverStateType::Intersecting, threshold, rootThreshold};
}
IntersectionObserverState::IntersectionObserverState(
IntersectionObserverStateType state)
: state_(state) {}
IntersectionObserverState::IntersectionObserverState(
IntersectionObserverStateType state,
Float threshold,
Float rootThreshold)
: state_(state), threshold_(threshold), rootThreshold_(rootThreshold) {}
bool IntersectionObserverState::isIntersecting() const {
return state_ == IntersectionObserverStateType::Intersecting;
}
bool IntersectionObserverState::operator==(
const IntersectionObserverState& other) const {
if (state_ != other.state_) {
return false;
}
if (state_ != IntersectionObserverStateType::Intersecting) {
return true;
}
return threshold_ == other.threshold_ &&
rootThreshold_ == other.rootThreshold_;
}
bool IntersectionObserverState::operator!=(
const IntersectionObserverState& other) const {
return !(*this == other);
}
} // namespace facebook::react

View File

@@ -0,0 +1,51 @@
/*
* 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/graphics/Float.h>
#include <optional>
namespace {
enum class IntersectionObserverStateType {
Initial,
NotIntersecting,
Intersecting,
};
}
namespace facebook::react {
class IntersectionObserverState {
public:
static IntersectionObserverState Initial();
static IntersectionObserverState NotIntersecting();
static IntersectionObserverState Intersecting(Float threshold);
static IntersectionObserverState Intersecting(Float threshold, Float rootThreshold);
bool isIntersecting() const;
bool operator==(const IntersectionObserverState &other) const;
bool operator!=(const IntersectionObserverState &other) const;
private:
explicit IntersectionObserverState(IntersectionObserverStateType state);
IntersectionObserverState(IntersectionObserverStateType state, Float threshold, Float rootThreshold);
IntersectionObserverStateType state_;
// This value is only relevant if the state is
// IntersectionObserverStateType::Intersecting.
Float threshold_{};
// This value is only relevant if the state is
// IntersectionObserverStateType::Intersecting.
std::optional<Float> rootThreshold_{};
};
} // namespace facebook::react