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,55 @@
# 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/internal/react-native-platform-selector.cmake)
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
react_native_android_selector(platform_SRC
platform/android/react/renderer/textlayoutmanager/*.cpp
platform/cxx/react/renderer/textlayoutmanager/*.cpp)
file(GLOB react_renderer_textlayourmanager_SRC CONFIGURE_DEPENDS
*.cpp
${platform_SRC})
add_library(react_renderer_textlayoutmanager
OBJECT
${react_renderer_textlayourmanager_SRC})
react_native_android_selector(platform_DIR
${CMAKE_CURRENT_SOURCE_DIR}/platform/android/
${CMAKE_CURRENT_SOURCE_DIR}/platform/cxx/)
target_include_directories(react_renderer_textlayoutmanager
PUBLIC
.
${REACT_COMMON_DIR}
${platform_DIR}
)
react_native_android_selector(fbjni fbjni "")
react_native_android_selector(mapbufferjni mapbufferjni "")
react_native_android_selector(reactnativejni reactnativejni "")
target_link_libraries(react_renderer_textlayoutmanager
glog
${fbjni}
folly_runtime
${mapbufferjni}
react_debug
react_renderer_attributedstring
react_renderer_componentregistry
react_renderer_core
react_renderer_debug
react_renderer_graphics
react_renderer_mapbuffer
react_renderer_mounting
react_renderer_telemetry
react_utils
${reactnativejni}
yoga
)
target_compile_reactnative_options(react_renderer_textlayoutmanager PRIVATE)
target_compile_options(react_renderer_textlayoutmanager PRIVATE -Wpedantic)

View File

@@ -0,0 +1,36 @@
/*
* 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/graphics/Float.h>
namespace facebook::react {
/*
* TextLayoutContext: Additional contextual information useful for text
* measurement.
*/
struct TextLayoutContext {
/*
* Reflects the scale factor needed to convert from the logical coordinate
* space into the device coordinate space of the physical screen.
* Some layout systems *might* use this to round layout metric values
* to `pixel value`.
*/
Float pointScaleFactor{1.0};
/**
* The ID of the surface being laid out
*/
SurfaceId surfaceId{-1};
bool operator==(const TextLayoutContext &rhs) const = default;
};
} // 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.
*/
#pragma once
#include <glog/logging.h>
#include <cstddef>
#include <react/renderer/attributedstring/AttributedStringBox.h>
#include <react/renderer/attributedstring/ParagraphAttributes.h>
#include <react/renderer/graphics/Size.h>
#include <react/renderer/textlayoutmanager/TextLayoutManager.h>
#include <react/renderer/textlayoutmanager/TextMeasureCache.h>
namespace facebook::react {
template <typename TextLayoutManagerT>
concept TextLayoutManagerWithPreparedLayout = requires(
TextLayoutManagerT textLayoutManager,
AttributedString attributedString,
ParagraphAttributes paragraphAttributes,
TextLayoutContext layoutContext,
LayoutConstraints layoutConstraints,
typename TextLayoutManagerT::PreparedLayout preparedLayout) {
sizeof(typename TextLayoutManagerT::PreparedLayout);
{
textLayoutManager.prepareLayout(attributedString, paragraphAttributes, layoutContext, layoutConstraints)
} -> std::same_as<typename TextLayoutManagerT::PreparedLayout>;
{
textLayoutManager.measurePreparedLayout(preparedLayout, layoutContext, layoutConstraints)
} -> std::same_as<TextMeasurement>;
};
namespace detail {
template <typename T>
struct PreparedLayoutT {
using type = std::nullptr_t;
};
template <TextLayoutManagerWithPreparedLayout T>
struct PreparedLayoutT<T> {
using type = typename T::PreparedLayout;
};
/**
* TextLayoutManagerExtended acts as an adapter for TextLayoutManager methods
* which may not exist for a specific platform. Callers can check at
* compile-time whether a method is supported, and calling if it is not will
* terminate.
*/
template <typename TextLayoutManagerT>
class TextLayoutManagerExtended {
public:
static constexpr bool supportsLineMeasurement()
{
return requires(TextLayoutManagerT textLayoutManager) {
{
textLayoutManager.measureLines(AttributedStringBox{}, ParagraphAttributes{}, Size{})
} -> std::same_as<LinesMeasurements>;
};
}
static constexpr bool supportsPreparedLayout()
{
return TextLayoutManagerWithPreparedLayout<TextLayoutManagerT>;
}
using PreparedLayout = typename PreparedLayoutT<TextLayoutManagerT>::type;
TextLayoutManagerExtended(const TextLayoutManagerT &textLayoutManager) : textLayoutManager_(textLayoutManager) {}
LinesMeasurements measureLines(
const AttributedStringBox &attributedStringBox,
const ParagraphAttributes &paragraphAttributes,
const Size &size)
{
if constexpr (supportsLineMeasurement()) {
return textLayoutManager_.measureLines(attributedStringBox, paragraphAttributes, size);
}
LOG(FATAL) << "Platform TextLayoutManager does not support measureLines";
}
PreparedLayout prepareLayout(
const AttributedString &attributedString,
const ParagraphAttributes &paragraphAttributes,
const TextLayoutContext &layoutContext,
const LayoutConstraints &layoutConstraints) const
{
if constexpr (supportsPreparedLayout()) {
return textLayoutManager_.prepareLayout(attributedString, paragraphAttributes, layoutContext, layoutConstraints);
}
LOG(FATAL) << "Platform TextLayoutManager does not support prepareLayout";
}
TextMeasurement measurePreparedLayout(
const PreparedLayout &layout,
const TextLayoutContext &layoutContext,
const LayoutConstraints &layoutConstraints) const
{
if constexpr (supportsPreparedLayout()) {
return textLayoutManager_.measurePreparedLayout(layout, layoutContext, layoutConstraints);
}
LOG(FATAL) << "Platform TextLayoutManager does not support measurePreparedLayout";
}
private:
const TextLayoutManagerT &textLayoutManager_;
};
} // namespace detail
using TextLayoutManagerExtended = detail::TextLayoutManagerExtended<TextLayoutManager>;
struct MeasuredPreparedLayout {
LayoutConstraints layoutConstraints;
TextMeasurement measurement;
TextLayoutManagerExtended::PreparedLayout preparedLayout{};
};
} // namespace facebook::react

View File

@@ -0,0 +1,68 @@
/*
* 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 "TextMeasureCache.h"
#include <utility>
namespace facebook::react {
static Rect rectFromDynamic(const folly::dynamic& data) {
Point origin;
origin.x = static_cast<Float>(data.getDefault("x", 0).getDouble());
origin.y = static_cast<Float>(data.getDefault("y", 0).getDouble());
Size size;
size.width = static_cast<Float>(data.getDefault("width", 0).getDouble());
size.height = static_cast<Float>(data.getDefault("height", 0).getDouble());
Rect frame;
frame.origin = origin;
frame.size = size;
return frame;
}
LineMeasurement::LineMeasurement(
std::string text,
Rect frame,
Float descender,
Float capHeight,
Float ascender,
Float xHeight)
: text(std::move(text)),
frame(frame),
descender(descender),
capHeight(capHeight),
ascender(ascender),
xHeight(xHeight) {}
LineMeasurement::LineMeasurement(const folly::dynamic& data)
: text(data.getDefault("text", "").getString()),
frame(rectFromDynamic(data)),
descender(
static_cast<Float>(data.getDefault("descender", 0).getDouble())),
capHeight(
static_cast<Float>(data.getDefault("capHeight", 0).getDouble())),
ascender(static_cast<Float>(data.getDefault("ascender", 0).getDouble())),
xHeight(static_cast<Float>(data.getDefault("xHeight", 0).getDouble())) {}
bool LineMeasurement::operator==(const LineMeasurement& rhs) const {
return std::tie(
this->text,
this->frame,
this->descender,
this->capHeight,
this->ascender,
this->xHeight) ==
std::tie(
rhs.text,
rhs.frame,
rhs.descender,
rhs.capHeight,
rhs.ascender,
rhs.xHeight);
}
} // namespace facebook::react

View File

@@ -0,0 +1,299 @@
/*
* 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/attributedstring/AttributedString.h>
#include <react/renderer/attributedstring/ParagraphAttributes.h>
#include <react/renderer/core/LayoutConstraints.h>
#include <react/utils/FloatComparison.h>
#include <react/utils/SimpleThreadSafeCache.h>
#include <react/utils/hash_combine.h>
namespace facebook::react {
struct LineMeasurement {
std::string text;
Rect frame;
Float descender;
Float capHeight;
Float ascender;
Float xHeight;
LineMeasurement(std::string text, Rect frame, Float descender, Float capHeight, Float ascender, Float xHeight);
LineMeasurement(const folly::dynamic &data);
bool operator==(const LineMeasurement &rhs) const;
static inline Float baseline(const std::vector<LineMeasurement> &lines)
{
if (!lines.empty()) {
return lines[0].ascender;
}
return 0;
}
};
using LinesMeasurements = std::vector<LineMeasurement>;
/*
* Describes a result of text measuring.
*/
class TextMeasurement final {
public:
class Attachment final {
public:
Rect frame;
bool isClipped;
};
using Attachments = std::vector<Attachment>;
Size size;
Attachments attachments;
};
// The Key type that is used for Text Measure Cache.
// The equivalence and hashing operations of this are defined to respect the
// nature of text measuring.
class TextMeasureCacheKey final {
public:
AttributedString attributedString{};
ParagraphAttributes paragraphAttributes{};
LayoutConstraints layoutConstraints{};
};
// The Key type that is used for Line Measure Cache.
// The equivalence and hashing operations of this are defined to respect the
// nature of text measuring.
class LineMeasureCacheKey final {
public:
AttributedString attributedString{};
ParagraphAttributes paragraphAttributes{};
Size size{};
};
/**
* Cache key, mapping an AttributedString under given constraints, to a prepared
* (laid out and drawable) representation of the text.
*/
class PreparedTextCacheKey final {
public:
AttributedString attributedString{};
ParagraphAttributes paragraphAttributes{};
LayoutConstraints layoutConstraints{};
};
/*
* Maximum size of the Cache.
* The number was empirically chosen based on approximation of an average amount
* of meaningful measures per surface.
*/
constexpr auto kSimpleThreadSafeCacheSizeCap = size_t{1024};
/*
* Thread-safe, evicting hash table designed to store text measurement
* information.
*/
using TextMeasureCache = SimpleThreadSafeCache<TextMeasureCacheKey, TextMeasurement, kSimpleThreadSafeCacheSizeCap>;
/*
* Thread-safe, evicting hash table designed to store line measurement
* information.
*/
using LineMeasureCache = SimpleThreadSafeCache<LineMeasureCacheKey, LinesMeasurements, kSimpleThreadSafeCacheSizeCap>;
inline bool areTextAttributesEquivalentLayoutWise(const TextAttributes &lhs, const TextAttributes &rhs)
{
// Here we check all attributes that affect layout metrics and don't check any
// attributes that affect only a decorative aspect of displayed text (like
// colors).
return std::tie(
lhs.fontFamily,
lhs.fontWeight,
lhs.fontStyle,
lhs.fontVariant,
lhs.allowFontScaling,
lhs.dynamicTypeRamp,
lhs.alignment) ==
std::tie(
rhs.fontFamily,
rhs.fontWeight,
rhs.fontStyle,
rhs.fontVariant,
rhs.allowFontScaling,
rhs.dynamicTypeRamp,
rhs.alignment) &&
floatEquality(lhs.fontSize, rhs.fontSize) && floatEquality(lhs.fontSizeMultiplier, rhs.fontSizeMultiplier) &&
floatEquality(lhs.letterSpacing, rhs.letterSpacing) && floatEquality(lhs.lineHeight, rhs.lineHeight);
}
inline size_t textAttributesHashLayoutWise(const TextAttributes &textAttributes)
{
// Taking into account the same props as
// `areTextAttributesEquivalentLayoutWise` mentions.
return facebook::react::hash_combine(
textAttributes.fontFamily,
textAttributes.fontSize,
textAttributes.fontSizeMultiplier,
textAttributes.fontWeight,
textAttributes.fontStyle,
textAttributes.fontVariant,
textAttributes.allowFontScaling,
textAttributes.dynamicTypeRamp,
textAttributes.letterSpacing,
textAttributes.lineHeight,
textAttributes.alignment);
}
inline bool areAttributedStringFragmentsEquivalentLayoutWise(
const AttributedString::Fragment &lhs,
const AttributedString::Fragment &rhs)
{
return lhs.string == rhs.string && areTextAttributesEquivalentLayoutWise(lhs.textAttributes, rhs.textAttributes) &&
// LayoutMetrics of an attachment fragment affects the size of a measured
// attributed string.
(!lhs.isAttachment() || (lhs.parentShadowView.layoutMetrics == rhs.parentShadowView.layoutMetrics));
}
inline bool areAttributedStringFragmentsEquivalentDisplayWise(
const AttributedString::Fragment &lhs,
const AttributedString::Fragment &rhs)
{
return lhs.isContentEqual(rhs) &&
// LayoutMetrics of an attachment fragment affects the size of a measured
// attributed string.
(!lhs.isAttachment() || (lhs.parentShadowView.layoutMetrics == rhs.parentShadowView.layoutMetrics));
}
inline size_t attributedStringFragmentHashLayoutWise(const AttributedString::Fragment &fragment)
{
// Here we are not taking `isAttachment` and `layoutMetrics` into account
// because they are logically interdependent and this can break an invariant
// between hash and equivalence functions (and cause cache misses).
return facebook::react::hash_combine(fragment.string, textAttributesHashLayoutWise(fragment.textAttributes));
}
inline size_t attributedStringFragmentHashDisplayWise(const AttributedString::Fragment &fragment)
{
// Here we are not taking `isAttachment` and `layoutMetrics` into account
// because they are logically interdependent and this can break an invariant
// between hash and equivalence functions (and cause cache misses).
return facebook::react::hash_combine(fragment.string, fragment.textAttributes);
}
inline bool areAttributedStringsEquivalentLayoutWise(const AttributedString &lhs, const AttributedString &rhs)
{
auto &lhsFragment = lhs.getFragments();
auto &rhsFragment = rhs.getFragments();
if (lhsFragment.size() != rhsFragment.size()) {
return false;
}
auto size = lhsFragment.size();
for (auto i = size_t{0}; i < size; i++) {
if (!areAttributedStringFragmentsEquivalentLayoutWise(lhsFragment.at(i), rhsFragment.at(i))) {
return false;
}
}
return true;
}
inline bool areAttributedStringsEquivalentDisplayWise(const AttributedString &lhs, const AttributedString &rhs)
{
auto &lhsFragment = lhs.getFragments();
auto &rhsFragment = rhs.getFragments();
if (lhsFragment.size() != rhsFragment.size()) {
return false;
}
auto size = lhsFragment.size();
for (size_t i = 0; i < size; i++) {
if (!areAttributedStringFragmentsEquivalentDisplayWise(lhsFragment.at(i), rhsFragment.at(i))) {
return false;
}
}
return true;
}
inline size_t attributedStringHashLayoutWise(const AttributedString &attributedString)
{
auto seed = size_t{0};
for (const auto &fragment : attributedString.getFragments()) {
facebook::react::hash_combine(seed, attributedStringFragmentHashLayoutWise(fragment));
}
return seed;
}
inline size_t attributedStringHashDisplayWise(const AttributedString &attributedString)
{
size_t seed = 0;
for (const auto &fragment : attributedString.getFragments()) {
facebook::react::hash_combine(seed, attributedStringFragmentHashDisplayWise(fragment));
}
return seed;
}
inline bool operator==(const TextMeasureCacheKey &lhs, const TextMeasureCacheKey &rhs)
{
return areAttributedStringsEquivalentLayoutWise(lhs.attributedString, rhs.attributedString) &&
lhs.paragraphAttributes == rhs.paragraphAttributes && lhs.layoutConstraints == rhs.layoutConstraints;
}
inline bool operator==(const LineMeasureCacheKey &lhs, const LineMeasureCacheKey &rhs)
{
return areAttributedStringsEquivalentLayoutWise(lhs.attributedString, rhs.attributedString) &&
lhs.paragraphAttributes == rhs.paragraphAttributes && lhs.size == rhs.size;
}
inline bool operator==(const PreparedTextCacheKey &lhs, const PreparedTextCacheKey &rhs)
{
return areAttributedStringsEquivalentDisplayWise(lhs.attributedString, rhs.attributedString) &&
lhs.paragraphAttributes == rhs.paragraphAttributes && lhs.layoutConstraints == rhs.layoutConstraints;
}
} // namespace facebook::react
namespace std {
template <>
struct hash<facebook::react::TextMeasureCacheKey> {
size_t operator()(const facebook::react::TextMeasureCacheKey &key) const
{
return facebook::react::hash_combine(
attributedStringHashLayoutWise(key.attributedString), key.paragraphAttributes, key.layoutConstraints);
}
};
template <>
struct hash<facebook::react::LineMeasureCacheKey> {
size_t operator()(const facebook::react::LineMeasureCacheKey &key) const
{
return facebook::react::hash_combine(
attributedStringHashLayoutWise(key.attributedString), key.paragraphAttributes, key.size);
}
};
template <>
struct hash<facebook::react::PreparedTextCacheKey> {
size_t operator()(const facebook::react::PreparedTextCacheKey &key) const
{
return facebook::react::hash_combine(
attributedStringHashDisplayWise(key.attributedString), key.paragraphAttributes, key.layoutConstraints);
}
};
} // namespace std

View File

@@ -0,0 +1,19 @@
/*
* 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 <fbjni/fbjni.h>
namespace facebook::react {
class JPreparedLayout : public jni::JavaClass<JPreparedLayout> {
public:
static auto constexpr kJavaDescriptor = "Lcom/facebook/react/views/text/PreparedLayout;";
};
} // namespace facebook::react

View File

@@ -0,0 +1,440 @@
/*
* 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 <span>
#include <utility>
#include <react/common/mapbuffer/JReadableMapBuffer.h>
#include <react/debug/react_native_assert.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/jni/ReadableNativeMap.h>
#include <react/renderer/attributedstring/conversions.h>
#include <react/renderer/core/conversions.h>
#include <react/renderer/mapbuffer/MapBuffer.h>
#include <react/renderer/mapbuffer/MapBufferBuilder.h>
#include <react/renderer/telemetry/TransactionTelemetry.h>
#include <react/renderer/textlayoutmanager/TextLayoutManager.h>
#include <react/renderer/textlayoutmanager/TextLayoutManagerExtended.h>
namespace facebook::react {
static_assert(TextLayoutManagerExtended::supportsLineMeasurement());
static_assert(TextLayoutManagerExtended::supportsPreparedLayout());
namespace {
int countAttachments(const AttributedString& attributedString) {
int count = 0;
for (const auto& fragment : attributedString.getFragments()) {
if (fragment.isAttachment()) {
count++;
}
}
return count;
}
Size measureText(
const std::shared_ptr<const ContextContainer>& contextContainer,
Tag rootTag,
MapBuffer attributedString,
MapBuffer paragraphAttributes,
float minWidth,
float maxWidth,
float minHeight,
float maxHeight,
jfloatArray attachmentPositions) {
const jni::global_ref<jobject>& fabricUIManager =
contextContainer->at<jni::global_ref<jobject>>("FabricUIManager");
static auto measure =
jni::findClassStatic("com/facebook/react/fabric/FabricUIManager")
->getMethod<jlong(
jint,
JReadableMapBuffer::javaobject,
JReadableMapBuffer::javaobject,
jfloat,
jfloat,
jfloat,
jfloat,
jfloatArray)>("measureText");
auto attributedStringBuffer =
JReadableMapBuffer::createWithContents(std::move(attributedString));
auto paragraphAttributesBuffer =
JReadableMapBuffer::createWithContents(std::move(paragraphAttributes));
return yogaMeassureToSize(measure(
fabricUIManager,
rootTag,
attributedStringBuffer.get(),
paragraphAttributesBuffer.get(),
minWidth,
maxWidth,
minHeight,
maxHeight,
attachmentPositions));
}
TextMeasurement doMeasure(
const std::shared_ptr<const ContextContainer>& contextContainer,
const AttributedString& attributedString,
const ParagraphAttributes& paragraphAttributes,
const TextLayoutContext& layoutContext,
const LayoutConstraints& layoutConstraints) {
const int attachmentCount = countAttachments(attributedString);
auto env = jni::Environment::current();
auto attachmentPositions = env->NewFloatArray(attachmentCount * 2);
auto minimumSize = layoutConstraints.minimumSize;
auto maximumSize = layoutConstraints.maximumSize;
// We assume max height will have no effect on measurement, so we override it
// with a constant value with no constraints, to enable cache reuse later down
// in the stack.
// TODO: This is suss, and not at the right layer
maximumSize.height = std::numeric_limits<Float>::infinity();
auto attributedStringMap = toMapBuffer(attributedString);
auto paragraphAttributesMap = toMapBuffer(paragraphAttributes);
auto size = measureText(
contextContainer,
layoutContext.surfaceId,
std::move(attributedStringMap),
std::move(paragraphAttributesMap),
minimumSize.width,
maximumSize.width,
minimumSize.height,
maximumSize.height,
attachmentPositions);
jfloat* attachmentDataElements =
env->GetFloatArrayElements(attachmentPositions, nullptr /*isCopy*/);
std::span<float> attachmentData{
attachmentDataElements, static_cast<size_t>(attachmentCount * 2)};
auto attachments = TextMeasurement::Attachments{};
if (attachmentCount > 0) {
for (const auto& fragment : attributedString.getFragments()) {
if (fragment.isAttachment()) {
float top = attachmentData[attachments.size() * 2];
float left = attachmentData[attachments.size() * 2 + 1];
if (std::isnan(top) || std::isnan(left)) {
attachments.push_back(
TextMeasurement::Attachment{.frame = Rect{}, .isClipped = true});
} else {
float width =
fragment.parentShadowView.layoutMetrics.frame.size.width;
float height =
fragment.parentShadowView.layoutMetrics.frame.size.height;
auto rect = facebook::react::Rect{
.origin = {.x = left, .y = top},
.size = facebook::react::Size{.width = width, .height = height}};
attachments.push_back(
TextMeasurement::Attachment{.frame = rect, .isClipped = false});
}
}
}
}
// Clean up allocated ref
env->ReleaseFloatArrayElements(
attachmentPositions, attachmentDataElements, JNI_ABORT);
env->DeleteLocalRef(attachmentPositions);
return TextMeasurement{.size = size, .attachments = attachments};
}
} // namespace
TextLayoutManager::TextLayoutManager(
const std::shared_ptr<const ContextContainer>& contextContainer)
: contextContainer_(std::move(contextContainer)),
textMeasureCache_(kSimpleThreadSafeCacheSizeCap),
lineMeasureCache_(kSimpleThreadSafeCacheSizeCap),
preparedTextCache_(
static_cast<size_t>(
ReactNativeFeatureFlags::preparedTextCacheSize())) {}
TextMeasurement TextLayoutManager::measure(
const AttributedStringBox& attributedStringBox,
const ParagraphAttributes& paragraphAttributes,
const TextLayoutContext& layoutContext,
const LayoutConstraints& layoutConstraints) const {
auto& attributedString = attributedStringBox.getValue();
auto measureText = [&]() {
auto telemetry = TransactionTelemetry::threadLocalTelemetry();
if (telemetry != nullptr) {
telemetry->willMeasureText();
}
auto measurement = doMeasure(
contextContainer_,
attributedString,
paragraphAttributes,
layoutContext,
layoutConstraints);
if (telemetry != nullptr) {
telemetry->didMeasureText();
}
return measurement;
};
auto measurement =
(ReactNativeFeatureFlags::disableTextLayoutManagerCacheAndroid() ||
ReactNativeFeatureFlags::enablePreparedTextLayout())
? measureText()
: textMeasureCache_.get(
{.attributedString = attributedString,
.paragraphAttributes = paragraphAttributes,
.layoutConstraints = layoutConstraints},
std::move(measureText));
measurement.size = layoutConstraints.clamp(measurement.size);
return measurement;
}
TextMeasurement TextLayoutManager::measureCachedSpannableById(
int64_t cacheId,
const ParagraphAttributes& paragraphAttributes,
const TextLayoutContext& layoutContext,
const LayoutConstraints& layoutConstraints) const {
auto env = jni::Environment::current();
auto attachmentPositions = env->NewFloatArray(0);
auto minimumSize = layoutConstraints.minimumSize;
auto maximumSize = layoutConstraints.maximumSize;
auto localDataBuilder = MapBufferBuilder();
// TODO: this is always sourced from an int, and Java expects an int
localDataBuilder.putInt(AS_KEY_CACHE_ID, static_cast<int32_t>(cacheId));
auto size = measureText(
contextContainer_,
layoutContext.surfaceId,
localDataBuilder.build(),
toMapBuffer(paragraphAttributes),
minimumSize.width,
maximumSize.width,
minimumSize.height,
maximumSize.height,
attachmentPositions);
// Clean up allocated ref - it still takes up space in the JNI ref table even
// though it's 0 length
env->DeleteLocalRef(attachmentPositions);
// TODO: currently we do not support attachments for cached IDs - should we?
auto attachments = TextMeasurement::Attachments{};
return TextMeasurement{.size = size, .attachments = attachments};
}
LinesMeasurements TextLayoutManager::measureLines(
const AttributedStringBox& attributedStringBox,
const ParagraphAttributes& paragraphAttributes,
const Size& size) const {
react_native_assert(
attributedStringBox.getMode() == AttributedStringBox::Mode::Value);
const auto& attributedString = attributedStringBox.getValue();
auto doMeasureLines = [&]() {
const jni::global_ref<jobject>& fabricUIManager =
contextContainer_->at<jni::global_ref<jobject>>("FabricUIManager");
static auto measureLines =
jni::findClassStatic("com/facebook/react/fabric/FabricUIManager")
->getMethod<NativeArray::javaobject(
JReadableMapBuffer::javaobject,
JReadableMapBuffer::javaobject,
jfloat,
jfloat)>("measureLines");
auto attributedStringMB =
JReadableMapBuffer::createWithContents(toMapBuffer(attributedString));
auto paragraphAttributesMB = JReadableMapBuffer::createWithContents(
toMapBuffer(paragraphAttributes));
auto array = measureLines(
fabricUIManager,
attributedStringMB.get(),
paragraphAttributesMB.get(),
size.width,
size.height);
auto dynamicArray = cthis(array)->consume();
LinesMeasurements lineMeasurements;
lineMeasurements.reserve(dynamicArray.size());
for (const auto& data : dynamicArray) {
lineMeasurements.emplace_back(data);
}
// Explicitly release smart pointers to free up space faster in JNI
// tables
attributedStringMB.reset();
paragraphAttributesMB.reset();
return lineMeasurements;
};
return ReactNativeFeatureFlags::disableTextLayoutManagerCacheAndroid()
? doMeasureLines()
: lineMeasureCache_.get(
{.attributedString = attributedString,
.paragraphAttributes = paragraphAttributes,
.size = size},
std::move(doMeasureLines));
}
TextLayoutManager::PreparedLayout TextLayoutManager::prepareLayout(
const AttributedString& attributedString,
const ParagraphAttributes& paragraphAttributes,
const TextLayoutContext& layoutContext,
const LayoutConstraints& layoutConstraints) const {
static auto prepareTextLayout =
jni::findClassStatic("com/facebook/react/fabric/FabricUIManager")
->getMethod<JPreparedLayout::javaobject(
jint,
JReadableMapBuffer::javaobject,
JReadableMapBuffer::javaobject,
jfloat,
jfloat,
jfloat,
jfloat)>("prepareTextLayout");
static auto reusePreparedLayoutWithNewReactTags =
jni::findClassStatic("com/facebook/react/fabric/FabricUIManager")
->getMethod<JPreparedLayout::javaobject(
JPreparedLayout::javaobject, jintArray)>(
"reusePreparedLayoutWithNewReactTags");
const auto [key, preparedText] = preparedTextCache_.getWithKey(
{.attributedString = attributedString,
.paragraphAttributes = paragraphAttributes,
.layoutConstraints = layoutConstraints},
[&]() {
const auto& fabricUIManager =
contextContainer_->at<jni::global_ref<jobject>>("FabricUIManager");
auto attributedStringMB = JReadableMapBuffer::createWithContents(
toMapBuffer(attributedString));
auto paragraphAttributesMB = JReadableMapBuffer::createWithContents(
toMapBuffer(paragraphAttributes));
auto minimumSize = layoutConstraints.minimumSize;
auto maximumSize = layoutConstraints.maximumSize;
return PreparedLayout{jni::make_global(prepareTextLayout(
fabricUIManager,
layoutContext.surfaceId,
attributedStringMB.get(),
paragraphAttributesMB.get(),
minimumSize.width,
maximumSize.width,
minimumSize.height,
maximumSize.height))};
});
// PreparedTextCacheKey allows equality of layouts which are the same
// display-wise, but ShadowView fragments (and thus react tags) may have
// changed.
const auto& fragments = attributedString.getFragments();
const auto& cacheKeyFragments = key->attributedString.getFragments();
bool needsNewReactTags = [&] {
for (size_t i = 0; i < fragments.size(); i++) {
if (fragments[i].parentShadowView.tag !=
cacheKeyFragments[i].parentShadowView.tag) {
return true;
}
}
return false;
}();
if (needsNewReactTags) {
std::vector<int> reactTags(fragments.size());
for (size_t i = 0; i < reactTags.size(); i++) {
reactTags[i] = fragments[i].parentShadowView.tag;
}
auto javaReactTags = jni::JArrayInt::newArray(fragments.size());
javaReactTags->setRegion(
0, static_cast<jsize>(reactTags.size()), reactTags.data());
const auto& fabricUIManager =
contextContainer_->at<jni::global_ref<jobject>>("FabricUIManager");
return PreparedLayout{jni::make_global(reusePreparedLayoutWithNewReactTags(
fabricUIManager, preparedText->get(), javaReactTags.get()))};
} else {
return PreparedLayout{*preparedText};
}
}
TextMeasurement TextLayoutManager::measurePreparedLayout(
const PreparedLayout& preparedLayout,
const TextLayoutContext& /*layoutContext*/,
const LayoutConstraints& layoutConstraints) const {
const auto& fabricUIManager =
contextContainer_->at<jni::global_ref<jobject>>("FabricUIManager");
static auto measurePreparedLayout =
jni::findClassStatic("com/facebook/react/fabric/FabricUIManager")
->getMethod<jni::JArrayFloat(
JPreparedLayout::javaobject, jfloat, jfloat, jfloat, jfloat)>(
"measurePreparedLayout");
auto minimumSize = layoutConstraints.minimumSize;
auto maximumSize = layoutConstraints.maximumSize;
auto measurementsArr = measurePreparedLayout(
fabricUIManager,
preparedLayout.get(),
minimumSize.width,
maximumSize.width,
minimumSize.height,
maximumSize.height);
auto measurements = measurementsArr->getRegion(
0, static_cast<jsize>(measurementsArr->size()));
react_native_assert(measurementsArr->size() >= 2);
react_native_assert((measurementsArr->size() - 2) % 4 == 0);
TextMeasurement textMeasurement;
textMeasurement.size.width = measurements[0];
textMeasurement.size.height = measurements[1];
if (measurementsArr->size() > 2) {
textMeasurement.attachments.reserve((measurementsArr->size() - 2) / 4);
for (size_t i = 2; i < measurementsArr->size(); i += 4) {
auto top = measurements[i];
auto left = measurements[i + 1];
auto width = measurements[i + 2];
auto height = measurements[i + 3];
if (std::isnan(top) || std::isnan(left)) {
textMeasurement.attachments.push_back(
TextMeasurement::Attachment{.frame = Rect{}, .isClipped = true});
} else {
textMeasurement.attachments.push_back(
TextMeasurement::Attachment{
.frame =
{.origin = {.x = left, .y = top},
.size = {.width = width, .height = height}},
.isClipped = false});
}
}
}
return textMeasurement;
}
} // namespace facebook::react

View File

@@ -0,0 +1,101 @@
/*
* 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/jni/SafeReleaseJniRef.h>
#include <react/renderer/attributedstring/AttributedStringBox.h>
#include <react/renderer/attributedstring/ParagraphAttributes.h>
#include <react/renderer/core/LayoutConstraints.h>
#include <react/renderer/textlayoutmanager/JPreparedLayout.h>
#include <react/renderer/textlayoutmanager/TextLayoutContext.h>
#include <react/renderer/textlayoutmanager/TextMeasureCache.h>
#include <react/utils/ContextContainer.h>
#include <fbjni/fbjni.h>
#include <memory>
namespace facebook::react {
class TextLayoutManager;
/*
* Cross platform facade for text measurement (e.g. Android-specific
* TextLayoutManager)
*/
class TextLayoutManager {
public:
using PreparedLayout = SafeReleaseJniRef<jni::global_ref<JPreparedLayout>>;
TextLayoutManager(const std::shared_ptr<const ContextContainer> &contextContainer);
/*
* Not copyable.
*/
TextLayoutManager(const TextLayoutManager &) = delete;
TextLayoutManager &operator=(const TextLayoutManager &) = delete;
/*
* Not movable.
*/
TextLayoutManager(TextLayoutManager &&) = delete;
TextLayoutManager &operator=(TextLayoutManager &&) = delete;
/*
* Measures `attributedString` using native text rendering infrastructure.
*/
TextMeasurement measure(
const AttributedStringBox &attributedStringBox,
const ParagraphAttributes &paragraphAttributes,
const TextLayoutContext &layoutContext,
const LayoutConstraints &layoutConstraints) const;
/**
* Measures an AttributedString on the platform, as identified by some
* opaque cache ID.
*/
TextMeasurement measureCachedSpannableById(
int64_t cacheId,
const ParagraphAttributes &paragraphAttributes,
const TextLayoutContext &layoutContext,
const LayoutConstraints &layoutConstraints) const;
/*
* Measures lines of `attributedString` using native text rendering
* infrastructure.
*/
LinesMeasurements measureLines(
const AttributedStringBox &attributedStringBox,
const ParagraphAttributes &paragraphAttributes,
const Size &size) const;
/**
* Create a platform representation of fully laid out text, to later be
* reused.
*/
PreparedLayout prepareLayout(
const AttributedString &attributedString,
const ParagraphAttributes &paragraphAttributes,
const TextLayoutContext &layoutContext,
const LayoutConstraints &layoutConstraints) const;
/**
* Derive text and attachment measurements from a PreparedLayout.
*/
TextMeasurement measurePreparedLayout(
const PreparedLayout &layout,
const TextLayoutContext &layoutContext,
const LayoutConstraints &layoutConstraints) const;
private:
std::shared_ptr<const ContextContainer> contextContainer_;
TextMeasureCache textMeasureCache_;
LineMeasureCache lineMeasureCache_;
SimpleThreadSafeCache<PreparedTextCacheKey, PreparedLayout, -1 /* Set dynamically*/> preparedTextCache_;
};
} // namespace facebook::react

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.
*/
#include "TextLayoutManager.h"
namespace facebook::react {
TextLayoutManager::TextLayoutManager(
const std::shared_ptr<const ContextContainer>& /*contextContainer*/)
: textMeasureCache_(kSimpleThreadSafeCacheSizeCap) {}
TextMeasurement TextLayoutManager::measure(
const AttributedStringBox& attributedStringBox,
const ParagraphAttributes& /*paragraphAttributes*/,
const TextLayoutContext& /*layoutContext*/,
const LayoutConstraints& layoutConstraints) const {
TextMeasurement::Attachments attachments;
for (const auto& fragment : attributedStringBox.getValue().getFragments()) {
if (fragment.isAttachment()) {
attachments.push_back(
TextMeasurement::Attachment{
.frame =
{.origin = {.x = 0, .y = 0},
.size = {.width = 0, .height = 0}},
.isClipped = false});
}
}
return TextMeasurement{
.size =
{.width = layoutConstraints.minimumSize.width,
.height = layoutConstraints.minimumSize.height},
.attachments = attachments};
}
} // namespace facebook::react

View File

@@ -0,0 +1,57 @@
/*
* 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/attributedstring/AttributedStringBox.h>
#include <react/renderer/attributedstring/ParagraphAttributes.h>
#include <react/renderer/core/LayoutConstraints.h>
#include <react/renderer/textlayoutmanager/TextLayoutContext.h>
#include <react/renderer/textlayoutmanager/TextMeasureCache.h>
#include <react/utils/ContextContainer.h>
#include <memory>
namespace facebook::react {
class TextLayoutManager;
/*
* Cross platform facade for text measurement (e.g. Android-specific
* TextLayoutManager)
*/
class TextLayoutManager {
public:
explicit TextLayoutManager(const std::shared_ptr<const ContextContainer> &contextContainer);
virtual ~TextLayoutManager() = default;
/*
* Not copyable.
*/
TextLayoutManager(const TextLayoutManager &) = delete;
TextLayoutManager &operator=(const TextLayoutManager &) = delete;
/*
* Not movable.
*/
TextLayoutManager(TextLayoutManager &&) = delete;
TextLayoutManager &operator=(TextLayoutManager &&) = delete;
/*
* Measures `attributedString` using native text rendering infrastructure.
*/
virtual TextMeasurement measure(
const AttributedStringBox &attributedStringBox,
const ParagraphAttributes &paragraphAttributes,
const TextLayoutContext &layoutContext,
const LayoutConstraints &layoutConstraints) const;
protected:
std::shared_ptr<const ContextContainer> contextContainer_;
TextMeasureCache textMeasureCache_;
};
} // namespace facebook::react

View File

@@ -0,0 +1,80 @@
/*
* 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.
*/
#import <UIKit/UIKit.h>
#include <react/renderer/attributedstring/AttributedString.h>
#include <react/renderer/attributedstring/AttributedStringBox.h>
#include <react/renderer/attributedstring/TextAttributes.h>
NS_ASSUME_NONNULL_BEGIN
NSString *const RCTAttributedStringIsHighlightedAttributeName = @"IsHighlighted";
NSString *const RCTAttributedStringEventEmitterKey = @"EventEmitter";
// String representation of either `role` or `accessibilityRole`
NSString *const RCTTextAttributesAccessibilityRoleAttributeName = @"AccessibilityRole";
/*
* Creates `NSTextAttributes` from given `facebook::react::TextAttributes`
*/
NSMutableDictionary<NSAttributedStringKey, id> *RCTNSTextAttributesFromTextAttributes(
const facebook::react::TextAttributes &textAttributes);
/*
* Conversions amond `NSAttributedString`, `AttributedString` and `AttributedStringBox`.
*/
NSAttributedString *RCTNSAttributedStringFromAttributedString(
const facebook::react::AttributedString &attributedString);
NSAttributedString *RCTNSAttributedStringFromAttributedStringBox(
const facebook::react::AttributedStringBox &attributedStringBox);
facebook::react::AttributedStringBox RCTAttributedStringBoxFromNSAttributedString(
NSAttributedString *nsAttributedString);
NSString *RCTNSStringFromStringApplyingTextTransform(NSString *string, facebook::react::TextTransform textTransform);
void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText);
/*
* Whether two `NSAttributedString` lead to the same underlying displayed text, even if they are not strictly equal.
* I.e. is one string substitutable for the other when backing a control (which may have some ignorable attributes
* provided).
*/
BOOL RCTIsAttributedStringEffectivelySame(
NSAttributedString *text1,
NSAttributedString *text2,
NSDictionary<NSAttributedStringKey, id> *insensitiveAttributes,
const facebook::react::TextAttributes &baseTextAttributes);
static inline NSData *RCTWrapEventEmitter(const facebook::react::SharedEventEmitter &eventEmitter)
{
auto eventEmitterPtr = new std::weak_ptr<const facebook::react::EventEmitter>(eventEmitter);
return [[NSData alloc] initWithBytesNoCopy:eventEmitterPtr
length:sizeof(eventEmitterPtr)
deallocator:^(void *ptrToDelete, NSUInteger) {
delete (std::weak_ptr<facebook::react::EventEmitter> *)ptrToDelete;
}];
}
static inline facebook::react::SharedEventEmitter RCTUnwrapEventEmitter(NSData *data)
{
if (data.length == 0) {
return nullptr;
}
auto weakPtr = dynamic_cast<std::weak_ptr<const facebook::react::EventEmitter> *>(
(std::weak_ptr<const facebook::react::EventEmitter> *)data.bytes);
if (weakPtr != nullptr) {
return weakPtr->lock();
}
return nullptr;
}
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,614 @@
/*
* 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.
*/
#import "RCTAttributedTextUtils.h"
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/renderer/components/view/accessibilityPropsConversions.h>
#include <react/renderer/core/LayoutableShadowNode.h>
#include <react/renderer/textlayoutmanager/RCTFontProperties.h>
#include <react/renderer/textlayoutmanager/RCTFontUtils.h>
#include <react/renderer/textlayoutmanager/RCTTextPrimitivesConversions.h>
#include <react/utils/ManagedObjectWrapper.h>
#include <array>
using namespace facebook::react;
inline static UIFontWeight RCTUIFontWeightFromInteger(NSInteger fontWeight)
{
assert(fontWeight > 50);
assert(fontWeight < 950);
static auto weights = std::to_array<UIFontWeight>({/* ~100 */ UIFontWeightUltraLight,
/* ~200 */ UIFontWeightThin,
/* ~300 */ UIFontWeightLight,
/* ~400 */ UIFontWeightRegular,
/* ~500 */ UIFontWeightMedium,
/* ~600 */ UIFontWeightSemibold,
/* ~700 */ UIFontWeightBold,
/* ~800 */ UIFontWeightHeavy,
/* ~900 */ UIFontWeightBlack});
// The expression is designed to convert something like 760 or 830 to 7.
return weights[(fontWeight + 50) / 100 - 1];
}
inline static UIFontTextStyle RCTUIFontTextStyleForDynamicTypeRamp(const DynamicTypeRamp &dynamicTypeRamp)
{
switch (dynamicTypeRamp) {
case DynamicTypeRamp::Caption2:
return UIFontTextStyleCaption2;
case DynamicTypeRamp::Caption1:
return UIFontTextStyleCaption1;
case DynamicTypeRamp::Footnote:
return UIFontTextStyleFootnote;
case DynamicTypeRamp::Subheadline:
return UIFontTextStyleSubheadline;
case DynamicTypeRamp::Callout:
return UIFontTextStyleCallout;
case DynamicTypeRamp::Body:
return UIFontTextStyleBody;
case DynamicTypeRamp::Headline:
return UIFontTextStyleHeadline;
case DynamicTypeRamp::Title3:
return UIFontTextStyleTitle3;
case DynamicTypeRamp::Title2:
return UIFontTextStyleTitle2;
case DynamicTypeRamp::Title1:
return UIFontTextStyleTitle1;
case DynamicTypeRamp::LargeTitle:
return UIFontTextStyleLargeTitle;
}
}
inline static CGFloat RCTBaseSizeForDynamicTypeRamp(const DynamicTypeRamp &dynamicTypeRamp)
{
// Values taken from
// https://developer.apple.com/design/human-interface-guidelines/foundations/typography/#specifications
switch (dynamicTypeRamp) {
case DynamicTypeRamp::Caption2:
return 11.0;
case DynamicTypeRamp::Caption1:
return 12.0;
case facebook::react::DynamicTypeRamp::Footnote:
return 13.0;
case facebook::react::DynamicTypeRamp::Subheadline:
return 15.0;
case facebook::react::DynamicTypeRamp::Callout:
return 16.0;
case facebook::react::DynamicTypeRamp::Body:
return 17.0;
case facebook::react::DynamicTypeRamp::Headline:
return 17.0;
case facebook::react::DynamicTypeRamp::Title3:
return 20.0;
case facebook::react::DynamicTypeRamp::Title2:
return 22.0;
case facebook::react::DynamicTypeRamp::Title1:
return 28.0;
case facebook::react::DynamicTypeRamp::LargeTitle:
return 34.0;
}
}
inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const TextAttributes &textAttributes)
{
if (textAttributes.allowFontScaling.value_or(true)) {
CGFloat fontSizeMultiplier = !isnan(textAttributes.fontSizeMultiplier) ? textAttributes.fontSizeMultiplier : 1.0;
if (textAttributes.dynamicTypeRamp.has_value()) {
DynamicTypeRamp dynamicTypeRamp = textAttributes.dynamicTypeRamp.value();
UIFontMetrics *fontMetrics =
[UIFontMetrics metricsForTextStyle:RCTUIFontTextStyleForDynamicTypeRamp(dynamicTypeRamp)];
// Using a specific font size reduces rounding errors from -scaledValueForValue:
CGFloat requestedSize =
isnan(textAttributes.fontSize) ? RCTBaseSizeForDynamicTypeRamp(dynamicTypeRamp) : textAttributes.fontSize;
fontSizeMultiplier = [fontMetrics scaledValueForValue:requestedSize] / requestedSize;
}
CGFloat maxFontSizeMultiplier =
!isnan(textAttributes.maxFontSizeMultiplier) ? textAttributes.maxFontSizeMultiplier : 0.0;
return maxFontSizeMultiplier >= 1.0 ? fminf(maxFontSizeMultiplier, fontSizeMultiplier) : fontSizeMultiplier;
} else {
return 1.0;
}
}
inline static UIFont *RCTEffectiveFontFromTextAttributes(const TextAttributes &textAttributes)
{
NSString *fontFamily = [NSString stringWithUTF8String:textAttributes.fontFamily.c_str()];
RCTFontProperties fontProperties;
fontProperties.family = fontFamily;
fontProperties.size = textAttributes.fontSize;
fontProperties.style = textAttributes.fontStyle.has_value()
? RCTFontStyleFromFontStyle(textAttributes.fontStyle.value())
: RCTFontStyleUndefined;
fontProperties.variant = textAttributes.fontVariant.has_value()
? RCTFontVariantFromFontVariant(textAttributes.fontVariant.value())
: RCTFontVariantUndefined;
fontProperties.weight = textAttributes.fontWeight.has_value()
? RCTUIFontWeightFromInteger((NSInteger)textAttributes.fontWeight.value())
: NAN;
fontProperties.sizeMultiplier = RCTEffectiveFontSizeMultiplierFromTextAttributes(textAttributes);
return RCTFontWithFontProperties(fontProperties);
}
inline static UIColor *RCTEffectiveForegroundColorFromTextAttributes(const TextAttributes &textAttributes)
{
UIColor *effectiveForegroundColor = RCTUIColorFromSharedColor(textAttributes.foregroundColor) ?: [UIColor blackColor];
if (!isnan(textAttributes.opacity)) {
effectiveForegroundColor = [effectiveForegroundColor
colorWithAlphaComponent:CGColorGetAlpha(effectiveForegroundColor.CGColor) * textAttributes.opacity];
}
return effectiveForegroundColor;
}
inline static UIColor *RCTEffectiveBackgroundColorFromTextAttributes(const TextAttributes &textAttributes)
{
UIColor *effectiveBackgroundColor = RCTUIColorFromSharedColor(textAttributes.backgroundColor);
if (effectiveBackgroundColor && !isnan(textAttributes.opacity)) {
effectiveBackgroundColor = [effectiveBackgroundColor
colorWithAlphaComponent:CGColorGetAlpha(effectiveBackgroundColor.CGColor) * textAttributes.opacity];
}
return effectiveBackgroundColor ?: [UIColor clearColor];
}
NSMutableDictionary<NSAttributedStringKey, id> *RCTNSTextAttributesFromTextAttributes(
const TextAttributes &textAttributes)
{
NSMutableDictionary<NSAttributedStringKey, id> *attributes = [NSMutableDictionary dictionaryWithCapacity:10];
// Font
UIFont *font = RCTEffectiveFontFromTextAttributes(textAttributes);
if (font) {
attributes[NSFontAttributeName] = font;
}
// Colors
UIColor *effectiveForegroundColor = RCTEffectiveForegroundColorFromTextAttributes(textAttributes);
if (textAttributes.foregroundColor || !isnan(textAttributes.opacity)) {
attributes[NSForegroundColorAttributeName] = effectiveForegroundColor;
}
if (textAttributes.backgroundColor || !isnan(textAttributes.opacity)) {
attributes[NSBackgroundColorAttributeName] = RCTEffectiveBackgroundColorFromTextAttributes(textAttributes);
}
// Kerning
if (!isnan(textAttributes.letterSpacing)) {
attributes[NSKernAttributeName] = @(textAttributes.letterSpacing);
}
// Paragraph Style
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
BOOL isParagraphStyleUsed = NO;
if (textAttributes.alignment.has_value()) {
TextAlignment textAlignment = textAttributes.alignment.value_or(TextAlignment::Natural);
if (textAttributes.layoutDirection.value_or(LayoutDirection::LeftToRight) == LayoutDirection::RightToLeft) {
if (textAlignment == TextAlignment::Right) {
textAlignment = TextAlignment::Left;
} else if (textAlignment == TextAlignment::Left) {
textAlignment = TextAlignment::Right;
}
}
paragraphStyle.alignment = RCTNSTextAlignmentFromTextAlignment(textAlignment);
isParagraphStyleUsed = YES;
}
if (textAttributes.baseWritingDirection.has_value()) {
paragraphStyle.baseWritingDirection =
RCTNSWritingDirectionFromWritingDirection(textAttributes.baseWritingDirection.value());
isParagraphStyleUsed = YES;
}
if (textAttributes.lineBreakStrategy.has_value()) {
paragraphStyle.lineBreakStrategy =
RCTNSLineBreakStrategyFromLineBreakStrategy(textAttributes.lineBreakStrategy.value());
isParagraphStyleUsed = YES;
}
if (textAttributes.lineBreakMode.has_value()) {
paragraphStyle.lineBreakMode = RCTNSLineBreakModeFromLineBreakMode(textAttributes.lineBreakMode.value());
isParagraphStyleUsed = YES;
}
if (!isnan(textAttributes.lineHeight)) {
CGFloat lineHeight = textAttributes.lineHeight * RCTEffectiveFontSizeMultiplierFromTextAttributes(textAttributes);
paragraphStyle.minimumLineHeight = lineHeight;
paragraphStyle.maximumLineHeight = lineHeight;
isParagraphStyleUsed = YES;
}
if (isParagraphStyleUsed) {
attributes[NSParagraphStyleAttributeName] = paragraphStyle;
}
// Decoration
if (textAttributes.textDecorationLineType.value_or(TextDecorationLineType::None) != TextDecorationLineType::None) {
auto textDecorationLineType = textAttributes.textDecorationLineType.value();
NSUnderlineStyle style = RCTNSUnderlineStyleFromTextDecorationStyle(
textAttributes.textDecorationStyle.value_or(TextDecorationStyle::Solid));
UIColor *textDecorationColor = RCTUIColorFromSharedColor(textAttributes.textDecorationColor);
// Underline
if (textDecorationLineType == TextDecorationLineType::Underline ||
textDecorationLineType == TextDecorationLineType::UnderlineStrikethrough) {
attributes[NSUnderlineStyleAttributeName] = @(style);
if (textDecorationColor) {
attributes[NSUnderlineColorAttributeName] = textDecorationColor;
}
}
// Strikethrough
if (textDecorationLineType == TextDecorationLineType::Strikethrough ||
textDecorationLineType == TextDecorationLineType::UnderlineStrikethrough) {
attributes[NSStrikethroughStyleAttributeName] = @(style);
if (textDecorationColor) {
attributes[NSStrikethroughColorAttributeName] = textDecorationColor;
}
}
}
// Shadow
if (textAttributes.textShadowOffset.has_value()) {
auto textShadowOffset = textAttributes.textShadowOffset.value();
NSShadow *shadow = [NSShadow new];
shadow.shadowOffset = CGSize{textShadowOffset.width, textShadowOffset.height};
if (!isnan(textAttributes.textShadowRadius)) {
shadow.shadowBlurRadius = textAttributes.textShadowRadius;
}
if (textAttributes.textShadowColor) {
shadow.shadowColor = RCTUIColorFromSharedColor(textAttributes.textShadowColor);
}
attributes[NSShadowAttributeName] = shadow;
}
// Special
if (textAttributes.isHighlighted.value_or(false)) {
attributes[RCTAttributedStringIsHighlightedAttributeName] = @YES;
}
if (textAttributes.role.has_value()) {
std::string roleStr = toString(textAttributes.role.value());
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = [NSString stringWithUTF8String:roleStr.c_str()];
} else if (textAttributes.accessibilityRole.has_value()) {
std::string roleStr = toString(textAttributes.accessibilityRole.value());
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = [NSString stringWithUTF8String:roleStr.c_str()];
}
return attributes;
}
static void RCTApplyBaselineOffsetForRange(NSMutableAttributedString *attributedText, NSRange attributedTextRange)
{
__block CGFloat maximumLineHeight = 0;
[attributedText enumerateAttribute:NSParagraphStyleAttributeName
inRange:attributedTextRange
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
usingBlock:^(NSParagraphStyle *paragraphStyle, __unused NSRange range, __unused BOOL *stop) {
if (!paragraphStyle) {
return;
}
maximumLineHeight = MAX(paragraphStyle.maximumLineHeight, maximumLineHeight);
}];
if (maximumLineHeight == 0) {
// `lineHeight` was not specified, nothing to do.
return;
}
__block CGFloat maximumFontLineHeight = 0;
[attributedText enumerateAttribute:NSFontAttributeName
inRange:attributedTextRange
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) {
if (!font) {
return;
}
maximumFontLineHeight = MAX(font.lineHeight, maximumFontLineHeight);
}];
if (maximumLineHeight < maximumFontLineHeight) {
return;
}
CGFloat baseLineOffset = (maximumLineHeight - maximumFontLineHeight) / 2.0;
[attributedText addAttribute:NSBaselineOffsetAttributeName value:@(baseLineOffset) range:attributedTextRange];
}
void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText)
{
if (ReactNativeFeatureFlags::enableIOSTextBaselineOffsetPerLine()) {
[attributedText.string
enumerateSubstringsInRange:NSMakeRange(0, attributedText.length)
options:NSStringEnumerationByLines | NSStringEnumerationSubstringNotRequired
usingBlock:^(
NSString *_Nullable substring,
NSRange substringRange,
NSRange enclosingRange,
BOOL *_Nonnull stop) {
RCTApplyBaselineOffsetForRange(attributedText, enclosingRange);
}];
} else {
RCTApplyBaselineOffsetForRange(attributedText, NSMakeRange(0, attributedText.length));
}
}
static NSMutableAttributedString *RCTNSAttributedStringFragmentFromFragment(
const AttributedString::Fragment &fragment,
UIImage *placeholderImage)
{
if (fragment.isAttachment()) {
auto layoutMetrics = fragment.parentShadowView.layoutMetrics;
CGRect bounds = {
.origin = {.x = layoutMetrics.frame.origin.x, .y = layoutMetrics.frame.origin.y},
.size = {.width = layoutMetrics.frame.size.width, .height = layoutMetrics.frame.size.height}};
NSTextAttachment *attachment = [NSTextAttachment new];
attachment.image = placeholderImage;
attachment.bounds = bounds;
return [[NSMutableAttributedString attributedStringWithAttachment:attachment] mutableCopy];
} else {
NSString *string = [NSString stringWithUTF8String:fragment.string.c_str()];
if (fragment.textAttributes.textTransform.has_value()) {
auto textTransform = fragment.textAttributes.textTransform.value();
string = RCTNSStringFromStringApplyingTextTransform(string, textTransform);
}
return [[NSMutableAttributedString alloc]
initWithString:string
attributes:RCTNSTextAttributesFromTextAttributes(fragment.textAttributes)];
}
}
static NSMutableAttributedString *RCTNSAttributedStringFragmentWithAttributesFromFragment(
const AttributedString::Fragment &fragment,
UIImage *placeholderImage)
{
auto nsAttributedStringFragment = RCTNSAttributedStringFragmentFromFragment(fragment, placeholderImage);
if (fragment.parentShadowView.componentHandle) {
auto eventEmitterWrapper = RCTWrapEventEmitter(fragment.parentShadowView.eventEmitter);
NSDictionary<NSAttributedStringKey, id> *additionalTextAttributes =
@{RCTAttributedStringEventEmitterKey : eventEmitterWrapper};
[nsAttributedStringFragment addAttributes:additionalTextAttributes
range:NSMakeRange(0, nsAttributedStringFragment.length)];
}
return nsAttributedStringFragment;
}
NSAttributedString *RCTNSAttributedStringFromAttributedString(const AttributedString &attributedString)
{
static UIImage *placeholderImage;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
placeholderImage = [UIImage new];
});
NSMutableAttributedString *nsAttributedString = [NSMutableAttributedString new];
[nsAttributedString beginEditing];
for (auto fragment : attributedString.getFragments()) {
NSMutableAttributedString *nsAttributedStringFragment =
RCTNSAttributedStringFragmentWithAttributesFromFragment(fragment, placeholderImage);
[nsAttributedString appendAttributedString:nsAttributedStringFragment];
}
[nsAttributedString endEditing];
return nsAttributedString;
}
NSAttributedString *RCTNSAttributedStringFromAttributedStringBox(const AttributedStringBox &attributedStringBox)
{
switch (attributedStringBox.getMode()) {
case AttributedStringBox::Mode::Value:
return RCTNSAttributedStringFromAttributedString(attributedStringBox.getValue());
case AttributedStringBox::Mode::OpaquePointer:
return (NSAttributedString *)unwrapManagedObject(attributedStringBox.getOpaquePointer());
}
}
AttributedStringBox RCTAttributedStringBoxFromNSAttributedString(NSAttributedString *nsAttributedString)
{
return nsAttributedString.length ? AttributedStringBox{wrapManagedObject(nsAttributedString)} : AttributedStringBox{};
}
static NSString *capitalizeText(NSString *text)
{
NSArray *words = [text componentsSeparatedByString:@" "];
NSMutableArray *newWords = [NSMutableArray new];
NSNumberFormatter *num = [NSNumberFormatter new];
for (NSString *item in words) {
NSString *word;
if ([item length] > 0 && [num numberFromString:[item substringWithRange:NSMakeRange(0, 1)]] == nil) {
word = [item capitalizedString];
} else {
word = [item lowercaseString];
}
[newWords addObject:word];
}
return [newWords componentsJoinedByString:@" "];
}
NSString *RCTNSStringFromStringApplyingTextTransform(NSString *string, TextTransform textTransform)
{
switch (textTransform) {
case TextTransform::Uppercase:
return [string uppercaseString];
case TextTransform::Lowercase:
return [string lowercaseString];
case TextTransform::Capitalize:
return capitalizeText(string);
default:
return string;
}
}
static BOOL RCTIsParagraphStyleEffectivelySame(
NSParagraphStyle *style1,
NSParagraphStyle *style2,
const TextAttributes &baseTextAttributes)
{
if (style1 == nil || style2 == nil) {
return style1 == nil && style2 == nil;
}
// The NSParagraphStyle included as part of typingAttributes may eventually resolve "natural" directions to
// physical direction, so we should compare resolved directions
auto naturalAlignment =
baseTextAttributes.layoutDirection.value_or(LayoutDirection::LeftToRight) == LayoutDirection::LeftToRight
? NSTextAlignmentLeft
: NSTextAlignmentRight;
NSWritingDirection naturalBaseWritingDirection = baseTextAttributes.baseWritingDirection.has_value()
? RCTNSWritingDirectionFromWritingDirection(baseTextAttributes.baseWritingDirection.value())
: [NSParagraphStyle defaultWritingDirectionForLanguage:nil];
if (style1.alignment == NSTextAlignmentNatural || style1.baseWritingDirection == NSWritingDirectionNatural) {
NSMutableParagraphStyle *mutableStyle1 = [style1 mutableCopy];
style1 = mutableStyle1;
if (mutableStyle1.alignment == NSTextAlignmentNatural) {
mutableStyle1.alignment = naturalAlignment;
}
if (mutableStyle1.baseWritingDirection == NSWritingDirectionNatural) {
mutableStyle1.baseWritingDirection = naturalBaseWritingDirection;
}
}
if (style2.alignment == NSTextAlignmentNatural || style2.baseWritingDirection == NSWritingDirectionNatural) {
NSMutableParagraphStyle *mutableStyle2 = [style2 mutableCopy];
style2 = mutableStyle2;
if (mutableStyle2.alignment == NSTextAlignmentNatural) {
mutableStyle2.alignment = naturalAlignment;
}
if (mutableStyle2.baseWritingDirection == NSWritingDirectionNatural) {
mutableStyle2.baseWritingDirection = naturalBaseWritingDirection;
}
}
return [style1 isEqual:style2];
}
static BOOL RCTIsAttributeEffectivelySame(
NSAttributedStringKey attributeKey,
NSDictionary<NSAttributedStringKey, id> *attributes1,
NSDictionary<NSAttributedStringKey, id> *attributes2,
NSDictionary<NSAttributedStringKey, id> *insensitiveAttributes,
const TextAttributes &baseTextAttributes)
{
id attribute1 = attributes1[attributeKey] ?: insensitiveAttributes[attributeKey];
id attribute2 = attributes2[attributeKey] ?: insensitiveAttributes[attributeKey];
// Normalize attributes which can inexact but still effectively the same
if ([attributeKey isEqualToString:NSParagraphStyleAttributeName]) {
return RCTIsParagraphStyleEffectivelySame(attribute1, attribute2, baseTextAttributes);
}
// Otherwise rely on built-in comparison
return [attribute1 isEqual:attribute2];
}
BOOL RCTIsAttributedStringEffectivelySame(
NSAttributedString *text1,
NSAttributedString *text2,
NSDictionary<NSAttributedStringKey, id> *insensitiveAttributes,
const TextAttributes &baseTextAttributes)
{
if (![text1.string isEqualToString:text2.string]) {
return NO;
}
// We check that for every fragment in the old string
// 1. The new string's fragment overlapping the first spans the same characters
// 2. The attributes of each matching fragment are the same, ignoring those which match insensitive attibutes
__block BOOL areAttributesSame = YES;
[text1 enumerateAttributesInRange:NSMakeRange(0, text1.length)
options:0
usingBlock:^(
NSDictionary<NSAttributedStringKey, id> *text1Attributes,
NSRange text1Range,
BOOL *text1Stop) {
[text2 enumerateAttributesInRange:text1Range
options:0
usingBlock:^(
NSDictionary<NSAttributedStringKey, id> *text2Attributes,
NSRange text2Range,
BOOL *text2Stop) {
if (!NSEqualRanges(text1Range, text2Range)) {
areAttributesSame = NO;
*text1Stop = YES;
*text2Stop = YES;
return;
}
// Compare every attribute in text1 to the corresponding attribute
// in text2, or the set of insensitive attributes if not present
for (NSAttributedStringKey key in text1Attributes) {
if (!RCTIsAttributeEffectivelySame(
key,
text1Attributes,
text2Attributes,
insensitiveAttributes,
baseTextAttributes)) {
areAttributesSame = NO;
*text1Stop = YES;
*text2Stop = YES;
return;
}
}
for (NSAttributedStringKey key in text2Attributes) {
// We have already compared this attribute if it is present in
// both
if (text1Attributes[key] != nil) {
continue;
}
// But we still need to compare attributes if it is only present
// in text 2, to compare against insensitive attributes
if (!RCTIsAttributeEffectivelySame(
key,
text1Attributes,
text2Attributes,
insensitiveAttributes,
baseTextAttributes)) {
areAttributesSame = NO;
*text1Stop = YES;
*text2Stop = YES;
return;
}
}
}];
}];
return areAttributesSame;
}

View File

@@ -0,0 +1,58 @@
/*
* 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.
*/
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, RCTFontStyle) {
RCTFontStyleUndefined = -1,
RCTFontStyleNormal,
RCTFontStyleItalic,
RCTFontStyleOblique,
};
typedef NS_OPTIONS(NSInteger, RCTFontVariant) {
RCTFontVariantUndefined = -1,
RCTFontVariantDefault = 0,
RCTFontVariantSmallCaps = 1 << 1,
RCTFontVariantOldstyleNums = 1 << 2,
RCTFontVariantLiningNums = 1 << 3,
RCTFontVariantTabularNums = 1 << 4,
RCTFontVariantProportionalNums = 1 << 5,
RCTFontVariantStylisticOne = 1 << 6,
RCTFontVariantStylisticTwo = 1 << 7,
RCTFontVariantStylisticThree = 1 << 8,
RCTFontVariantStylisticFour = 1 << 9,
RCTFontVariantStylisticFive = 1 << 10,
RCTFontVariantStylisticSix = 1 << 11,
RCTFontVariantStylisticSeven = 1 << 12,
RCTFontVariantStylisticEight = 1 << 13,
RCTFontVariantStylisticNine = 1 << 14,
RCTFontVariantStylisticTen = 1 << 15,
RCTFontVariantStylisticEleven = 1 << 16,
RCTFontVariantStylisticTwelve = 1 << 17,
RCTFontVariantStylisticThirteen = 1 << 18,
RCTFontVariantStylisticFourteen = 1 << 19,
RCTFontVariantStylisticFifteen = 1 << 20,
RCTFontVariantStylisticSixteen = 1 << 21,
RCTFontVariantStylisticSeventeen = 1 << 22,
RCTFontVariantStylisticEighteen = 1 << 23,
RCTFontVariantStylisticNineteen = 1 << 24,
RCTFontVariantStylisticTwenty = 1 << 25,
};
struct RCTFontProperties {
NSString *family = nil;
CGFloat size = NAN;
UIFontWeight weight = NAN;
RCTFontStyle style = RCTFontStyleUndefined;
RCTFontVariant variant = RCTFontVariantUndefined;
CGFloat sizeMultiplier = NAN;
};
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,27 @@
/*
* 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.
*/
#import <React/RCTDefines.h>
#import <UIKit/UIKit.h>
#import <react/renderer/textlayoutmanager/RCTFontProperties.h>
NS_ASSUME_NONNULL_BEGIN
using RCTDefaultFontResolver = UIFont *__nullable (^)(const RCTFontProperties &);
/**
* React Native will use the System font for rendering by default. If you want to
* provide a different base font, use this override.
*/
RCT_EXTERN void RCTSetDefaultFontResolver(RCTDefaultFontResolver handler);
/**
* Returns UIFont instance corresponded to given font properties.
*/
RCT_EXTERN UIFont *RCTFontWithFontProperties(RCTFontProperties fontProperties);
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,406 @@
/*
* 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.
*/
#import "RCTFontUtils.h"
#import <CoreText/CoreText.h>
#import <React/RCTFont+Private.h>
#import <React/RCTFont.h>
#import <algorithm>
#import <cmath>
#import <limits>
#import <map>
#import <mutex>
static RCTFontProperties RCTDefaultFontProperties()
{
static RCTFontProperties defaultFontProperties;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
defaultFontProperties.family = [UIFont systemFontOfSize:defaultFontProperties.size].familyName;
defaultFontProperties.size = 14;
defaultFontProperties.weight = UIFontWeightRegular;
defaultFontProperties.style = RCTFontStyleNormal;
defaultFontProperties.variant = RCTFontVariantDefault;
defaultFontProperties.sizeMultiplier = 1.0;
});
return defaultFontProperties;
}
static RCTFontProperties RCTResolveFontProperties(
RCTFontProperties fontProperties,
RCTFontProperties baseFontProperties)
{
fontProperties.family = (fontProperties.family.length != 0u) ? fontProperties.family : baseFontProperties.family;
fontProperties.size = !isnan(fontProperties.size) ? fontProperties.size : baseFontProperties.size;
fontProperties.weight = !isnan(fontProperties.weight) ? fontProperties.weight : baseFontProperties.weight;
fontProperties.style =
fontProperties.style != RCTFontStyleUndefined ? fontProperties.style : baseFontProperties.style;
fontProperties.variant =
fontProperties.variant != RCTFontVariantUndefined ? fontProperties.variant : baseFontProperties.variant;
return fontProperties;
}
static RCTFontStyle RCTGetFontStyle(UIFont *font)
{
NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute];
UIFontDescriptorSymbolicTraits symbolicTraits = [traits[UIFontSymbolicTrait] unsignedIntValue];
if ((symbolicTraits & UIFontDescriptorTraitItalic) != 0u) {
return RCTFontStyleItalic;
}
return RCTFontStyleNormal;
}
static NSArray *RCTFontFeatures(RCTFontVariant fontVariant)
{
NSMutableArray *fontFeatures = [NSMutableArray array];
static std::map<RCTFontVariant, NSDictionary *> mapping;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mapping = {
{RCTFontVariantSmallCaps, @{
UIFontFeatureTypeIdentifierKey : @(kLowerCaseType),
UIFontFeatureSelectorIdentifierKey : @(kLowerCaseSmallCapsSelector),
}},
{RCTFontVariantOldstyleNums, @{
UIFontFeatureTypeIdentifierKey : @(kNumberCaseType),
UIFontFeatureSelectorIdentifierKey : @(kLowerCaseNumbersSelector),
}},
{RCTFontVariantLiningNums, @{
UIFontFeatureTypeIdentifierKey : @(kNumberCaseType),
UIFontFeatureSelectorIdentifierKey : @(kUpperCaseNumbersSelector),
}},
{RCTFontVariantTabularNums, @{
UIFontFeatureTypeIdentifierKey : @(kNumberSpacingType),
UIFontFeatureSelectorIdentifierKey : @(kMonospacedNumbersSelector),
}},
{RCTFontVariantProportionalNums, @{
UIFontFeatureTypeIdentifierKey : @(kNumberSpacingType),
UIFontFeatureSelectorIdentifierKey : @(kProportionalNumbersSelector),
}},
{RCTFontVariantStylisticOne, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltOneOnSelector),
}},
{RCTFontVariantStylisticTwo, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltTwoOnSelector),
}},
{RCTFontVariantStylisticThree, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltThreeOnSelector),
}},
{RCTFontVariantStylisticFour, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltFourOnSelector),
}},
{RCTFontVariantStylisticFive, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltFiveOnSelector),
}},
{RCTFontVariantStylisticSix, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltSixOnSelector),
}},
{RCTFontVariantStylisticSeven, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltSevenOnSelector),
}},
{RCTFontVariantStylisticEight, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltEightOnSelector),
}},
{RCTFontVariantStylisticNine, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltNineOnSelector),
}},
{RCTFontVariantStylisticTen, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltTenOnSelector),
}},
{RCTFontVariantStylisticEleven, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltElevenOnSelector),
}},
{RCTFontVariantStylisticTwelve, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltTwelveOnSelector),
}},
{RCTFontVariantStylisticThirteen, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltThirteenOnSelector),
}},
{RCTFontVariantStylisticFourteen, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltFourteenOnSelector),
}},
{RCTFontVariantStylisticFifteen, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltFifteenOnSelector),
}},
{RCTFontVariantStylisticSixteen, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltSixteenOnSelector),
}},
{RCTFontVariantStylisticSeventeen, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltSeventeenOnSelector),
}},
{RCTFontVariantStylisticEighteen, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltEighteenOnSelector),
}},
{RCTFontVariantStylisticNineteen, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltNineteenOnSelector),
}},
{RCTFontVariantStylisticTwenty, @{
UIFontFeatureTypeIdentifierKey : @(kStylisticAlternativesType),
UIFontFeatureSelectorIdentifierKey : @(kStylisticAltTwentyOnSelector),
}},
};
});
if ((fontVariant & RCTFontVariantSmallCaps) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantSmallCaps]];
}
if ((fontVariant & RCTFontVariantOldstyleNums) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantOldstyleNums]];
}
if ((fontVariant & RCTFontVariantLiningNums) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantLiningNums]];
}
if ((fontVariant & RCTFontVariantTabularNums) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantTabularNums]];
}
if ((fontVariant & RCTFontVariantProportionalNums) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantProportionalNums]];
}
if ((fontVariant & RCTFontVariantStylisticOne) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticOne]];
}
if ((fontVariant & RCTFontVariantStylisticTwo) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticTwo]];
}
if ((fontVariant & RCTFontVariantStylisticThree) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticThree]];
}
if ((fontVariant & RCTFontVariantStylisticFour) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticFour]];
}
if ((fontVariant & RCTFontVariantStylisticFive) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticFive]];
}
if ((fontVariant & RCTFontVariantStylisticSix) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticSix]];
}
if ((fontVariant & RCTFontVariantStylisticSeven) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticSeven]];
}
if ((fontVariant & RCTFontVariantStylisticEight) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticEight]];
}
if ((fontVariant & RCTFontVariantStylisticNine) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticNine]];
}
if ((fontVariant & RCTFontVariantStylisticTen) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticTen]];
}
if ((fontVariant & RCTFontVariantStylisticEleven) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticEleven]];
}
if ((fontVariant & RCTFontVariantStylisticTwelve) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticTwelve]];
}
if ((fontVariant & RCTFontVariantStylisticThirteen) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticThirteen]];
}
if ((fontVariant & RCTFontVariantStylisticFourteen) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticFourteen]];
}
if ((fontVariant & RCTFontVariantStylisticFifteen) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticFifteen]];
}
if ((fontVariant & RCTFontVariantStylisticSixteen) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticSixteen]];
}
if ((fontVariant & RCTFontVariantStylisticSeventeen) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticSeventeen]];
}
if ((fontVariant & RCTFontVariantStylisticEighteen) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticEighteen]];
}
if ((fontVariant & RCTFontVariantStylisticNineteen) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticNineteen]];
}
if ((fontVariant & RCTFontVariantStylisticTwenty) != 0) {
[fontFeatures addObject:mapping[RCTFontVariantStylisticTwenty]];
}
return fontFeatures;
}
static RCTDefaultFontResolver defaultFontResolver;
void RCTSetDefaultFontResolver(RCTDefaultFontResolver handler)
{
defaultFontResolver = handler;
}
static UIFont *RCTDefaultFontWithFontProperties(const RCTFontProperties &fontProperties)
{
static NSCache *fontCache;
static std::mutex fontCacheMutex;
CGFloat effectiveFontSize = fontProperties.sizeMultiplier * fontProperties.size;
NSString *cacheKey = [NSString stringWithFormat:@"%@/%.1f/%.2f/%ld",
fontProperties.family,
effectiveFontSize,
fontProperties.weight,
(long)fontProperties.style];
UIFont *font;
{
std::lock_guard<std::mutex> lock(fontCacheMutex);
if (fontCache == nil) {
fontCache = [NSCache new];
}
font = [fontCache objectForKey:cacheKey];
}
if (font == nil) {
if (defaultFontResolver != nil) {
font = defaultFontResolver(fontProperties);
}
if (font == nil) {
font = RCTGetLegacyDefaultFont(effectiveFontSize, fontProperties.weight);
}
if (font == nil) {
font = [UIFont systemFontOfSize:effectiveFontSize weight:fontProperties.weight];
}
BOOL isItalicFont = fontProperties.style == RCTFontStyleItalic;
BOOL isCondensedFont = [fontProperties.family isEqualToString:@"SystemCondensed"];
if (isItalicFont || isCondensedFont) {
UIFontDescriptor *fontDescriptor = [font fontDescriptor];
UIFontDescriptorSymbolicTraits symbolicTraits = fontDescriptor.symbolicTraits;
if (isItalicFont) {
symbolicTraits |= UIFontDescriptorTraitItalic;
}
if (isCondensedFont) {
symbolicTraits |= UIFontDescriptorTraitCondensed;
}
fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits];
font = [UIFont fontWithDescriptor:fontDescriptor size:effectiveFontSize];
}
{
std::lock_guard<std::mutex> lock(fontCacheMutex);
[fontCache setObject:font forKey:cacheKey];
}
}
return font;
}
static UIFontDescriptorSystemDesign RCTGetFontDescriptorSystemDesign(NSString *family)
{
static NSDictionary<NSString *, NSString *> *systemDesigns = @{
@"system-ui" : UIFontDescriptorSystemDesignDefault,
@"ui-sans-serif" : UIFontDescriptorSystemDesignDefault,
@"ui-serif" : UIFontDescriptorSystemDesignSerif,
@"ui-rounded" : UIFontDescriptorSystemDesignRounded,
@"ui-monospace" : UIFontDescriptorSystemDesignMonospaced
};
return systemDesigns[family];
}
UIFont *RCTFontWithFontProperties(RCTFontProperties fontProperties)
{
RCTFontProperties defaultFontProperties = RCTDefaultFontProperties();
fontProperties = RCTResolveFontProperties(fontProperties, defaultFontProperties);
assert(!isnan(fontProperties.sizeMultiplier));
CGFloat effectiveFontSize = fontProperties.sizeMultiplier * fontProperties.size;
UIFont *font;
UIFontDescriptorSystemDesign design = RCTGetFontDescriptorSystemDesign([fontProperties.family lowercaseString]);
if (design != nullptr) {
// Create a system font which `-fontDescriptorWithDesign:` asks for
// (see:
// https://developer.apple.com/documentation/uikit/uifontdescriptor/3151797-fontdescriptorwithdesign?language=objc)
// It's OK to use `RCTDefaultFontWithFontProperties` which creates a system font
font = RCTDefaultFontWithFontProperties(fontProperties);
UIFontDescriptor *descriptor = [font.fontDescriptor fontDescriptorWithDesign:design];
font = [UIFont fontWithDescriptor:descriptor size:effectiveFontSize];
} else if ([fontProperties.family isEqualToString:defaultFontProperties.family]) {
// Handle system font as special case. This ensures that we preserve
// the specific metrics of the standard system font as closely as possible.
font = RCTDefaultFontWithFontProperties(fontProperties);
} else {
NSArray<NSString *> *fontNames = [UIFont fontNamesForFamilyName:fontProperties.family];
UIFontWeight fontWeight = fontProperties.weight;
if (fontNames.count == 0) {
// Gracefully handle being given a font name rather than font family, for
// example: "Helvetica Light Oblique" rather than just "Helvetica".
font = [UIFont fontWithName:fontProperties.family size:effectiveFontSize];
if (font != nullptr) {
fontNames = [UIFont fontNamesForFamilyName:font.familyName];
fontWeight = (fontWeight != 0.0) ?: RCTGetFontWeight(font);
} else {
// Failback to system font.
font = RCTDefaultFontWithFontProperties(fontProperties);
}
}
if (fontNames.count > 0) {
// Get the closest font that matches the given weight for the fontFamily
CGFloat closestWeight = INFINITY;
for (NSString *name in fontNames) {
UIFont *fontMatch = [UIFont fontWithName:name size:effectiveFontSize];
if (RCTGetFontStyle(fontMatch) != fontProperties.style) {
continue;
}
CGFloat testWeight = RCTGetFontWeight(fontMatch);
if (ABS(testWeight - fontWeight) < ABS(closestWeight - fontWeight)) {
font = fontMatch;
closestWeight = testWeight;
}
}
if (font == nil) {
// If we still don't have a match at least return the first font in the
// fontFamily This is to support built-in font Zapfino and other custom
// single font families like Impact
font = [UIFont fontWithName:fontNames[0] size:effectiveFontSize];
}
}
}
// Apply font variants to font object.
if (fontProperties.variant != RCTFontVariantDefault) {
NSArray *fontFeatures = RCTFontFeatures(fontProperties.variant);
UIFontDescriptor *fontDescriptor = [font.fontDescriptor
fontDescriptorByAddingAttributes:@{UIFontDescriptorFeatureSettingsAttribute : fontFeatures}];
font = [UIFont fontWithDescriptor:fontDescriptor size:effectiveFontSize];
}
return font;
}

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.
*/
#import <UIKit/UIKit.h>
#import <react/renderer/attributedstring/AttributedString.h>
#import <react/renderer/attributedstring/ParagraphAttributes.h>
#import <react/renderer/core/LayoutConstraints.h>
#import <react/renderer/textlayoutmanager/TextLayoutContext.h>
#import <react/renderer/textlayoutmanager/TextMeasureCache.h>
NS_ASSUME_NONNULL_BEGIN
/**
@abstract Enumeration block for text fragments.
*/
using RCTTextLayoutFragmentEnumerationBlock =
void (^)(CGRect fragmentRect, NSString *_Nonnull fragmentText, NSString *value);
/**
* iOS-specific TextLayoutManager
*/
@interface RCTTextLayoutManager : NSObject
- (facebook::react::TextMeasurement)measureAttributedString:(facebook::react::AttributedString)attributedString
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
layoutContext:(facebook::react::TextLayoutContext)layoutContext
layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints;
- (facebook::react::TextMeasurement)measureNSAttributedString:(NSAttributedString *)attributedString
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
layoutContext:(facebook::react::TextLayoutContext)layoutContext
layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints;
- (void)drawAttributedString:(facebook::react::AttributedString)attributedString
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
frame:(CGRect)frame
drawHighlightPath:(void (^_Nullable)(UIBezierPath *highlightPath))block;
- (facebook::react::LinesMeasurements)getLinesForAttributedString:(facebook::react::AttributedString)attributedString
paragraphAttributes:
(facebook::react::ParagraphAttributes)paragraphAttributes
size:(CGSize)size;
- (facebook::react::SharedEventEmitter)
getEventEmitterWithAttributeString:(facebook::react::AttributedString)attributedString
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
frame:(CGRect)frame
atPoint:(CGPoint)point;
- (void)getRectWithAttributedString:(facebook::react::AttributedString)attributedString
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
enumerateAttribute:(NSString *)enumerateAttribute
frame:(CGRect)frame
usingBlock:(RCTTextLayoutFragmentEnumerationBlock)block;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,436 @@
/*
* 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.
*/
#import "RCTTextLayoutManager.h"
#import "RCTAttributedTextUtils.h"
#import <React/NSTextStorage+FontScaling.h>
#import <React/RCTUtils.h>
#import <react/featureflags/ReactNativeFeatureFlags.h>
#import <react/utils/ManagedObjectWrapper.h>
#import <react/utils/SimpleThreadSafeCache.h>
using namespace facebook::react;
@implementation RCTTextLayoutManager {
SimpleThreadSafeCache<AttributedString, std::shared_ptr<void>, 256> _cache;
}
static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsizeMode)
{
switch (ellipsizeMode) {
case EllipsizeMode::Clip:
return NSLineBreakByClipping;
case EllipsizeMode::Head:
return NSLineBreakByTruncatingHead;
case EllipsizeMode::Tail:
return NSLineBreakByTruncatingTail;
case EllipsizeMode::Middle:
return NSLineBreakByTruncatingMiddle;
}
}
- (TextMeasurement)measureNSAttributedString:(NSAttributedString *)attributedString
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
layoutContext:(TextLayoutContext)layoutContext
layoutConstraints:(LayoutConstraints)layoutConstraints
{
if (attributedString.length == 0) {
// This is not really an optimization because that should be checked much earlier on the call stack.
// Sometimes, very irregularly, measuring an empty string crashes/freezes iOS internal text infrastructure.
// This is our last line of defense.
return {};
}
CGSize maximumSize = CGSize{layoutConstraints.maximumSize.width, CGFLOAT_MAX};
NSTextStorage *textStorage = [self _textStorageAndLayoutManagerWithAttributesString:attributedString
paragraphAttributes:paragraphAttributes
size:maximumSize];
return [self _measureTextStorage:textStorage paragraphAttributes:paragraphAttributes layoutContext:layoutContext];
}
- (TextMeasurement)measureAttributedString:(AttributedString)attributedString
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
layoutContext:(TextLayoutContext)layoutContext
layoutConstraints:(LayoutConstraints)layoutConstraints
{
return [self measureNSAttributedString:[self _nsAttributedStringFromAttributedString:attributedString]
paragraphAttributes:paragraphAttributes
layoutContext:layoutContext
layoutConstraints:layoutConstraints];
}
- (void)drawAttributedString:(AttributedString)attributedString
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
frame:(CGRect)frame
drawHighlightPath:(void (^_Nullable)(UIBezierPath *highlightPath))block
{
NSTextStorage *textStorage = [self
_textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString]
paragraphAttributes:paragraphAttributes
size:frame.size];
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
#if TARGET_OS_MACCATALYST
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextSetShouldSmoothFonts(context, NO);
#endif
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
[self processTruncatedAttributedText:textStorage textContainer:textContainer layoutManager:layoutManager];
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:frame.origin];
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:frame.origin];
#if TARGET_OS_MACCATALYST
CGContextRestoreGState(context);
#endif
if (block != nil) {
__block UIBezierPath *highlightPath = nil;
NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
[textStorage
enumerateAttribute:RCTAttributedStringIsHighlightedAttributeName
inRange:characterRange
options:0
usingBlock:^(NSNumber *value, NSRange range, __unused BOOL *stop) {
if (!value.boolValue) {
return;
}
[layoutManager
enumerateEnclosingRectsForGlyphRange:range
withinSelectedGlyphRange:range
inTextContainer:textContainer
usingBlock:^(CGRect enclosingRect, __unused BOOL *anotherStop) {
UIBezierPath *path = [UIBezierPath
bezierPathWithRoundedRect:CGRectInset(enclosingRect, -2, -2)
cornerRadius:2];
if (highlightPath != nullptr) {
[highlightPath appendPath:path];
} else {
highlightPath = path;
}
}];
}];
block(highlightPath);
}
}
- (void)processTruncatedAttributedText:(NSTextStorage *)textStorage
textContainer:(NSTextContainer *)textContainer
layoutManager:(NSLayoutManager *)layoutManager
{
if (textContainer.maximumNumberOfLines > 0) {
[layoutManager ensureLayoutForTextContainer:textContainer];
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
__block int line = 0;
[layoutManager
enumerateLineFragmentsForGlyphRange:glyphRange
usingBlock:^(
CGRect rect,
CGRect usedRect,
NSTextContainer *_Nonnull _,
NSRange lineGlyphRange,
BOOL *_Nonnull stop) {
if (line == textContainer.maximumNumberOfLines - 1) {
NSRange truncatedRange = [layoutManager
truncatedGlyphRangeInLineFragmentForGlyphAtIndex:lineGlyphRange.location];
if (truncatedRange.location != NSNotFound) {
NSRange characterRange =
[layoutManager characterRangeForGlyphRange:truncatedRange
actualGlyphRange:nil];
if (characterRange.location > 0 && characterRange.length > 0) {
// Remove color attributes for truncated range
for (NSAttributedStringKey key in
@[ NSForegroundColorAttributeName, NSBackgroundColorAttributeName ]) {
[textStorage removeAttribute:key range:characterRange];
id attribute = [textStorage attribute:key
atIndex:characterRange.location - 1
effectiveRange:nil];
if (attribute != nullptr) {
[textStorage addAttribute:key value:attribute range:characterRange];
}
}
}
}
}
line++;
}];
}
}
- (LinesMeasurements)getLinesForAttributedString:(facebook::react::AttributedString)attributedString
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
size:(CGSize)size
{
NSTextStorage *textStorage = [self
_textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString]
paragraphAttributes:paragraphAttributes
size:size];
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
std::vector<LineMeasurement> paragraphLines{};
auto blockParagraphLines = &paragraphLines;
[layoutManager
enumerateLineFragmentsForGlyphRange:glyphRange
usingBlock:^(
CGRect overallRect,
CGRect usedRect,
NSTextContainer *_Nonnull usedTextContainer,
NSRange lineGlyphRange,
BOOL *_Nonnull stop) {
NSRange range = [layoutManager characterRangeForGlyphRange:lineGlyphRange
actualGlyphRange:nil];
NSString *renderedString = [textStorage.string substringWithRange:range];
UIFont *font =
[[textStorage attributedSubstringFromRange:range] attribute:NSFontAttributeName
atIndex:0
effectiveRange:nil];
auto rect = facebook::react::Rect{
.origin = facebook::react::Point{.x = usedRect.origin.x, .y = usedRect.origin.y},
.size = facebook::react::Size{
.width = usedRect.size.width, .height = usedRect.size.height}};
CGFloat baseline = [layoutManager locationForGlyphAtIndex:range.location].y;
auto line = LineMeasurement{
std::string([renderedString UTF8String]),
rect,
overallRect.size.height - baseline,
font.capHeight,
baseline,
font.xHeight};
blockParagraphLines->push_back(line);
}];
return paragraphLines;
}
- (NSTextStorage *)_textStorageAndLayoutManagerWithAttributesString:(NSAttributedString *)attributedString
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
size:(CGSize)size
{
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:size];
textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5.
textContainer.lineBreakMode = paragraphAttributes.maximumNumberOfLines > 0
? RCTNSLineBreakModeFromEllipsizeMode(paragraphAttributes.ellipsizeMode)
: NSLineBreakByClipping;
textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines;
NSLayoutManager *layoutManager = [NSLayoutManager new];
layoutManager.usesFontLeading = NO;
[layoutManager addTextContainer:textContainer];
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
RCTApplyBaselineOffset(textStorage);
[textStorage addLayoutManager:layoutManager];
if (paragraphAttributes.adjustsFontSizeToFit) {
CGFloat minimumFontSize = !isnan(paragraphAttributes.minimumFontSize) ? paragraphAttributes.minimumFontSize : 4.0;
CGFloat maximumFontSize = !isnan(paragraphAttributes.maximumFontSize) ? paragraphAttributes.maximumFontSize : 96.0;
[textStorage scaleFontSizeToFitSize:size minimumFontSize:minimumFontSize maximumFontSize:maximumFontSize];
}
return textStorage;
}
- (SharedEventEmitter)getEventEmitterWithAttributeString:(AttributedString)attributedString
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
frame:(CGRect)frame
atPoint:(CGPoint)point
{
NSTextStorage *textStorage = [self
_textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString]
paragraphAttributes:paragraphAttributes
size:frame.size];
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
CGFloat fraction;
NSUInteger characterIndex = [layoutManager characterIndexForPoint:point
inTextContainer:textContainer
fractionOfDistanceBetweenInsertionPoints:&fraction];
// If the point is not before (fraction == 0.0) the first character and not
// after (fraction == 1.0) the last character, then the attribute is valid.
if (textStorage.length > 0 && (fraction > 0 || characterIndex > 0) &&
(fraction < 1 || characterIndex < textStorage.length - 1)) {
NSData *eventEmitterWrapper = (NSData *)[textStorage attribute:RCTAttributedStringEventEmitterKey
atIndex:characterIndex
effectiveRange:NULL];
return RCTUnwrapEventEmitter(eventEmitterWrapper);
}
return nil;
}
- (void)getRectWithAttributedString:(AttributedString)attributedString
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
enumerateAttribute:(NSString *)enumerateAttribute
frame:(CGRect)frame
usingBlock:(RCTTextLayoutFragmentEnumerationBlock)block
{
NSTextStorage *textStorage = [self
_textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString]
paragraphAttributes:paragraphAttributes
size:frame.size];
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
[layoutManager ensureLayoutForTextContainer:textContainer];
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
[textStorage enumerateAttribute:enumerateAttribute
inRange:characterRange
options:0
usingBlock:^(NSString *value, NSRange range, BOOL *pause) {
if (value == nullptr) {
return;
}
[layoutManager
enumerateEnclosingRectsForGlyphRange:range
withinSelectedGlyphRange:range
inTextContainer:textContainer
usingBlock:^(CGRect enclosingRect, BOOL *_Nonnull stop) {
block(
enclosingRect,
[textStorage attributedSubstringFromRange:range].string,
value);
*stop = YES;
}];
}];
}
#pragma mark - Private
- (NSAttributedString *)_nsAttributedStringFromAttributedString:(AttributedString)attributedString
{
auto sharedNSAttributedString = _cache.get(attributedString, [&]() {
return wrapManagedObject(RCTNSAttributedStringFromAttributedString(attributedString));
});
return unwrapManagedObject(sharedNSAttributedString);
}
- (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
layoutContext:(TextLayoutContext)layoutContext
{
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
[layoutManager ensureLayoutForTextContainer:textContainer];
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
__block BOOL textDidWrap = NO;
__block NSUInteger linesEnumerated = 0;
__block CGFloat enumeratedLinesHeight = 0;
[layoutManager
enumerateLineFragmentsForGlyphRange:glyphRange
usingBlock:^(
CGRect overallRect,
CGRect usedRect,
NSTextContainer *_Nonnull usedTextContainer,
NSRange lineGlyphRange,
BOOL *_Nonnull stop) {
NSRange range = [layoutManager characterRangeForGlyphRange:lineGlyphRange
actualGlyphRange:nil];
NSUInteger lastCharacterIndex = range.location + range.length - 1;
BOOL endsWithNewLine =
[textStorage.string characterAtIndex:lastCharacterIndex] == '\n';
if (!endsWithNewLine && textStorage.string.length > lastCharacterIndex + 1) {
textDidWrap = YES;
}
if (linesEnumerated++ < paragraphAttributes.maximumNumberOfLines) {
enumeratedLinesHeight = usedRect.origin.y + usedRect.size.height;
}
if (textDidWrap &&
(paragraphAttributes.maximumNumberOfLines == 0 ||
linesEnumerated >= paragraphAttributes.maximumNumberOfLines)) {
*stop = YES;
}
}];
CGSize size = [layoutManager usedRectForTextContainer:textContainer].size;
if (textDidWrap) {
size.width = textContainer.size.width;
}
if (paragraphAttributes.maximumNumberOfLines != 0) {
// If maximumNumberOfLines is set, we cannot rely on setting it on the NSTextContainer
// due to an edge case where it returns wrong height:
// When maximumNumberOfLines is set to N and the N+1 line is empty, the measured height
// is N+1 lines (incorrect). Adding any characted to the N+1 line, making it non-empty
// casuses the measured height to be N lines (correct).
if (linesEnumerated < paragraphAttributes.maximumNumberOfLines) {
enumeratedLinesHeight += layoutManager.extraLineFragmentUsedRect.size.height;
}
size.height = enumeratedLinesHeight;
}
size = (CGSize){ceil(size.width * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor,
ceil(size.height * layoutContext.pointScaleFactor) / layoutContext.pointScaleFactor};
__block auto attachments = TextMeasurement::Attachments{};
[textStorage
enumerateAttribute:NSAttachmentAttributeName
inRange:NSMakeRange(0, textStorage.length)
options:0
usingBlock:^(NSTextAttachment *attachment, NSRange range, BOOL *stop) {
if (attachment == nullptr) {
return;
}
NSRange attachmentGlyphRange = [layoutManager glyphRangeForCharacterRange:range
actualCharacterRange:NULL];
NSRange truncatedRange =
[layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:attachmentGlyphRange.location];
if (truncatedRange.location != NSNotFound && attachmentGlyphRange.location >= truncatedRange.location) {
attachments.push_back(TextMeasurement::Attachment{.isClipped = true});
} else {
CGSize attachmentSize = attachment.bounds.size;
CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range inTextContainer:textContainer];
CGRect frame;
UIFont *font = [[textStorage attributedSubstringFromRange:range] attribute:NSFontAttributeName
atIndex:0
effectiveRange:nil];
frame = {
.origin =
{glyphRect.origin.x,
glyphRect.origin.y + glyphRect.size.height - attachmentSize.height + font.descender},
.size = attachmentSize};
auto rect = facebook::react::Rect{
.origin = facebook::react::Point{.x = frame.origin.x, .y = frame.origin.y},
.size = facebook::react::Size{.width = frame.size.width, .height = frame.size.height}};
attachments.push_back(TextMeasurement::Attachment{.frame = rect, .isClipped = false});
}
}];
return TextMeasurement{.size = {.width = size.width, .height = size.height}, .attachments = attachments};
}
@end

View File

@@ -0,0 +1,120 @@
/*
* 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.
*/
#import <UIKit/UIKit.h>
#include <react/renderer/graphics/RCTPlatformColorUtils.h>
#include <react/renderer/textlayoutmanager/RCTFontProperties.h>
#include <react/renderer/textlayoutmanager/RCTFontUtils.h>
inline static NSTextAlignment RCTNSTextAlignmentFromTextAlignment(facebook::react::TextAlignment textAlignment)
{
switch (textAlignment) {
case facebook::react::TextAlignment::Natural:
return NSTextAlignmentNatural;
case facebook::react::TextAlignment::Left:
return NSTextAlignmentLeft;
case facebook::react::TextAlignment::Right:
return NSTextAlignmentRight;
case facebook::react::TextAlignment::Center:
return NSTextAlignmentCenter;
case facebook::react::TextAlignment::Justified:
return NSTextAlignmentJustified;
}
}
inline static NSWritingDirection RCTNSWritingDirectionFromWritingDirection(
facebook::react::WritingDirection writingDirection)
{
switch (writingDirection) {
case facebook::react::WritingDirection::Natural:
return NSWritingDirectionNatural;
case facebook::react::WritingDirection::LeftToRight:
return NSWritingDirectionLeftToRight;
case facebook::react::WritingDirection::RightToLeft:
return NSWritingDirectionRightToLeft;
}
}
inline static NSLineBreakStrategy RCTNSLineBreakStrategyFromLineBreakStrategy(
facebook::react::LineBreakStrategy lineBreakStrategy)
{
switch (lineBreakStrategy) {
case facebook::react::LineBreakStrategy::None:
return NSLineBreakStrategyNone;
case facebook::react::LineBreakStrategy::PushOut:
return NSLineBreakStrategyPushOut;
case facebook::react::LineBreakStrategy::HangulWordPriority:
if (@available(iOS 14.0, *)) {
return NSLineBreakStrategyHangulWordPriority;
} else {
return NSLineBreakStrategyNone;
}
case facebook::react::LineBreakStrategy::Standard:
if (@available(iOS 14.0, *)) {
return NSLineBreakStrategyStandard;
} else {
return NSLineBreakStrategyNone;
}
}
}
inline static NSLineBreakMode RCTNSLineBreakModeFromLineBreakMode(facebook::react::LineBreakMode lineBreakMode)
{
switch (lineBreakMode) {
case facebook::react::LineBreakMode::Word:
return NSLineBreakByWordWrapping;
case facebook::react::LineBreakMode::Char:
return NSLineBreakByCharWrapping;
case facebook::react::LineBreakMode::Clip:
return NSLineBreakByClipping;
case facebook::react::LineBreakMode::Head:
return NSLineBreakByTruncatingHead;
case facebook::react::LineBreakMode::Middle:
return NSLineBreakByTruncatingMiddle;
case facebook::react::LineBreakMode::Tail:
return NSLineBreakByTruncatingTail;
}
}
inline static RCTFontStyle RCTFontStyleFromFontStyle(facebook::react::FontStyle fontStyle)
{
switch (fontStyle) {
case facebook::react::FontStyle::Normal:
return RCTFontStyleNormal;
case facebook::react::FontStyle::Italic:
return RCTFontStyleItalic;
case facebook::react::FontStyle::Oblique:
return RCTFontStyleOblique;
}
}
inline static RCTFontVariant RCTFontVariantFromFontVariant(facebook::react::FontVariant fontVariant)
{
return (RCTFontVariant)fontVariant;
}
inline static NSUnderlineStyle RCTNSUnderlineStyleFromTextDecorationStyle(
facebook::react::TextDecorationStyle textDecorationStyle)
{
switch (textDecorationStyle) {
case facebook::react::TextDecorationStyle::Solid:
return NSUnderlineStyleSingle;
case facebook::react::TextDecorationStyle::Double:
return NSUnderlineStyleDouble;
case facebook::react::TextDecorationStyle::Dashed:
return NSUnderlineStylePatternDash | NSUnderlineStyleSingle;
case facebook::react::TextDecorationStyle::Dotted:
return NSUnderlineStylePatternDot | NSUnderlineStyleSingle;
}
}
// TODO: this file has some duplicates method, we can remove it
inline static UIColor *_Nullable RCTUIColorFromSharedColor(const facebook::react::SharedColor &sharedColor)
{
return RCTPlatformColorFromColor(*sharedColor);
}

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 <react/renderer/attributedstring/AttributedStringBox.h>
#include <react/renderer/attributedstring/ParagraphAttributes.h>
#include <react/renderer/core/LayoutConstraints.h>
#include <react/renderer/textlayoutmanager/TextLayoutContext.h>
#include <react/renderer/textlayoutmanager/TextMeasureCache.h>
#include <react/utils/ContextContainer.h>
#include <memory>
namespace facebook::react {
/*
* Cross platform facade for text measurement (e.g. Android-specific
* TextLayoutManager)
*/
class TextLayoutManager {
public:
TextLayoutManager(const std::shared_ptr<const ContextContainer> &contextContainer);
/*
* Not copyable.
*/
TextLayoutManager(const TextLayoutManager &) = delete;
TextLayoutManager &operator=(const TextLayoutManager &) = delete;
/*
* Not movable.
*/
TextLayoutManager(TextLayoutManager &&) = delete;
TextLayoutManager &operator=(TextLayoutManager &&) = delete;
/*
* Measures `attributedString` using native text rendering infrastructure.
*/
TextMeasurement measure(
const AttributedStringBox &attributedStringBox,
const ParagraphAttributes &paragraphAttributes,
const TextLayoutContext &layoutContext,
const LayoutConstraints &layoutConstraints) const;
/*
* Measures lines of `attributedString` using native text rendering
* infrastructure.
*/
LinesMeasurements measureLines(
const AttributedStringBox &attributedStringBox,
const ParagraphAttributes &paragraphAttributes,
const Size &size) const;
/*
* Returns an opaque pointer to platform-specific TextLayoutManager.
* Is used on a native views layer to delegate text rendering to the manager.
*/
std::shared_ptr<void> getNativeTextLayoutManager() const;
protected:
std::shared_ptr<const ContextContainer> contextContainer_;
std::shared_ptr<void> nativeTextLayoutManager_;
TextMeasureCache textMeasureCache_;
LineMeasureCache lineMeasureCache_;
};
} // namespace facebook::react

View File

@@ -0,0 +1,114 @@
/*
* 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.
*/
#import "TextLayoutManager.h"
#import "RCTTextLayoutManager.h"
#import <react/renderer/attributedstring/PlaceholderAttributedString.h>
#import <react/renderer/telemetry/TransactionTelemetry.h>
#import <react/utils/ManagedObjectWrapper.h>
namespace facebook::react {
TextLayoutManager::TextLayoutManager(const std::shared_ptr<const ContextContainer> & /*contextContainer*/)
{
nativeTextLayoutManager_ = wrapManagedObject([RCTTextLayoutManager new]);
}
std::shared_ptr<void> TextLayoutManager::getNativeTextLayoutManager() const
{
assert(nativeTextLayoutManager_ && "Stored NativeTextLayoutManager must not be null.");
return nativeTextLayoutManager_;
}
TextMeasurement TextLayoutManager::measure(
const AttributedStringBox &attributedStringBox,
const ParagraphAttributes &paragraphAttributes,
const TextLayoutContext &layoutContext,
const LayoutConstraints &layoutConstraints) const
{
RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(nativeTextLayoutManager_);
auto measurement = TextMeasurement{};
switch (attributedStringBox.getMode()) {
case AttributedStringBox::Mode::Value: {
auto attributedString = ensurePlaceholderIfEmpty_DO_NOT_USE(attributedStringBox.getValue());
measurement = textMeasureCache_.get(
{.attributedString = attributedString,
.paragraphAttributes = paragraphAttributes,
.layoutConstraints = layoutConstraints},
[&]() {
auto telemetry = TransactionTelemetry::threadLocalTelemetry();
if (telemetry) {
telemetry->willMeasureText();
}
auto measurement = [textLayoutManager measureAttributedString:attributedString
paragraphAttributes:paragraphAttributes
layoutContext:layoutContext
layoutConstraints:layoutConstraints];
if (telemetry) {
telemetry->didMeasureText();
}
return measurement;
});
break;
}
case AttributedStringBox::Mode::OpaquePointer: {
NSAttributedString *nsAttributedString =
(NSAttributedString *)unwrapManagedObject(attributedStringBox.getOpaquePointer());
auto telemetry = TransactionTelemetry::threadLocalTelemetry();
if (telemetry != nullptr) {
telemetry->willMeasureText();
}
measurement = [textLayoutManager measureNSAttributedString:nsAttributedString
paragraphAttributes:paragraphAttributes
layoutContext:layoutContext
layoutConstraints:layoutConstraints];
if (telemetry != nullptr) {
telemetry->didMeasureText();
}
break;
}
}
measurement.size = layoutConstraints.clamp(measurement.size);
return measurement;
}
LinesMeasurements TextLayoutManager::measureLines(
const AttributedStringBox &attributedStringBox,
const ParagraphAttributes &paragraphAttributes,
const Size &size) const
{
react_native_assert(attributedStringBox.getMode() == AttributedStringBox::Mode::Value);
auto attributedString = ensurePlaceholderIfEmpty_DO_NOT_USE(attributedStringBox.getValue());
RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(nativeTextLayoutManager_);
auto measurement = lineMeasureCache_.get(
{.attributedString = attributedString, .paragraphAttributes = paragraphAttributes, .size = size}, [&]() {
auto measurement = [textLayoutManager getLinesForAttributedString:attributedString
paragraphAttributes:paragraphAttributes
size:{size.width, size.height}];
return measurement;
});
return measurement;
}
} // namespace facebook::react

View File

@@ -0,0 +1,18 @@
/*
* 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/textlayoutmanager/TextLayoutManager.h>
using namespace facebook::react;
TEST(TextLayoutManagerTest, testSomething) {
// TODO:
}