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

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

View File

@@ -0,0 +1,51 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
add_compile_options(
-fexceptions
-frtti
-std=c++20
-Wall
-Wpedantic
-DLOG_TAG=\"Fabric\")
file(GLOB react_render_textlayourmanager_SRC CONFIGURE_DEPENDS
*.cpp
platform/android/react/renderer/textlayoutmanager/*.cpp)
add_library(react_render_textlayoutmanager
STATIC
${react_render_textlayourmanager_SRC})
target_include_directories(react_render_textlayoutmanager
PUBLIC
${REACT_COMMON_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/platform/android/
)
target_link_libraries(react_render_textlayoutmanager
glog
fb
fbjni
folly_runtime
glog_init
mapbufferjni
react_debug
react_render_attributedstring
react_render_componentregistry
react_render_core
react_render_debug
react_render_graphics
react_render_mapbuffer
react_render_mounting
react_render_telemetry
react_render_uimanager
react_utils
reactnativejni
yoga
)

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <react/renderer/graphics/Float.h>
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};
};
inline bool operator==(
TextLayoutContext const& lhs,
TextLayoutContext const& rhs) {
return std::tie(lhs.pointScaleFactor) == std::tie(rhs.pointScaleFactor);
}
inline bool operator!=(
TextLayoutContext const& lhs,
TextLayoutContext const& rhs) {
return !(lhs == rhs);
}
} // 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,216 @@
/*
* 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;
};
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{};
};
/*
* 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>;
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 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 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 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 bool operator==(
const TextMeasureCacheKey& lhs,
const TextMeasureCacheKey& rhs) {
return areAttributedStringsEquivalentLayoutWise(
lhs.attributedString, rhs.attributedString) &&
lhs.paragraphAttributes == rhs.paragraphAttributes &&
lhs.layoutConstraints.maximumSize.width ==
rhs.layoutConstraints.maximumSize.width;
}
inline bool operator!=(
const TextMeasureCacheKey& lhs,
const TextMeasureCacheKey& rhs) {
return !(lhs == rhs);
}
} // 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.maximumSize.width);
}
};
} // namespace std

View File

@@ -0,0 +1,400 @@
/*
* 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"
#include <limits>
#include <react/common/mapbuffer/JReadableMapBuffer.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/utils/CoreFeatures.h>
using namespace facebook::jni;
namespace facebook::react {
static int countAttachments(const AttributedString& attributedString) {
int count = 0;
for (const auto& fragment : attributedString.getFragments()) {
if (fragment.isAttachment()) {
count++;
}
}
return count;
}
Size measureAndroidComponent(
const ContextContainer::Shared& contextContainer,
Tag rootTag,
const std::string& componentName,
folly::dynamic localData,
folly::dynamic props,
folly::dynamic state,
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,
jstring,
ReadableMap::javaobject,
ReadableMap::javaobject,
ReadableMap::javaobject,
jfloat,
jfloat,
jfloat,
jfloat,
jfloatArray)>("measure");
auto componentNameRef = make_jstring(componentName);
local_ref<ReadableNativeMap::javaobject> localDataRNM =
ReadableNativeMap::newObjectCxxArgs(std::move(localData));
local_ref<ReadableNativeMap::javaobject> propsRNM =
ReadableNativeMap::newObjectCxxArgs(std::move(props));
local_ref<ReadableNativeMap::javaobject> stateRNM =
ReadableNativeMap::newObjectCxxArgs(std::move(state));
local_ref<ReadableMap::javaobject> localDataRM =
make_local(reinterpret_cast<ReadableMap::javaobject>(localDataRNM.get()));
local_ref<ReadableMap::javaobject> propsRM =
make_local(reinterpret_cast<ReadableMap::javaobject>(propsRNM.get()));
local_ref<ReadableMap::javaobject> stateRM =
make_local(reinterpret_cast<ReadableMap::javaobject>(stateRNM.get()));
auto size = yogaMeassureToSize(measure(
fabricUIManager,
rootTag,
componentNameRef.get(),
localDataRM.get(),
propsRM.get(),
stateRM.get(),
minWidth,
maxWidth,
minHeight,
maxHeight,
attachmentPositions));
// Explicitly release smart pointers to free up space faster in JNI tables
componentNameRef.reset();
localDataRM.reset();
localDataRNM.reset();
propsRM.reset();
propsRNM.reset();
stateRM.reset();
stateRNM.reset();
return size;
}
Size measureAndroidComponentMapBuffer(
const ContextContainer::Shared& contextContainer,
Tag rootTag,
const std::string& componentName,
MapBuffer localData,
MapBuffer props,
float minWidth,
float maxWidth,
float minHeight,
float maxHeight,
jfloatArray attachmentPositions) {
const jni::global_ref<jobject>& fabricUIManager =
contextContainer->at<jni::global_ref<jobject>>("FabricUIManager");
auto componentNameRef = make_jstring(componentName);
static auto measure =
jni::findClassStatic("com/facebook/react/fabric/FabricUIManager")
->getMethod<jlong(
jint,
jstring,
JReadableMapBuffer::javaobject,
JReadableMapBuffer::javaobject,
JReadableMapBuffer::javaobject,
jfloat,
jfloat,
jfloat,
jfloat,
jfloatArray)>("measureMapBuffer");
auto localDataMap =
JReadableMapBuffer::createWithContents(std::move(localData));
auto propsMap = JReadableMapBuffer::createWithContents(std::move(props));
auto size = yogaMeassureToSize(measure(
fabricUIManager,
rootTag,
componentNameRef.get(),
localDataMap.get(),
propsMap.get(),
nullptr,
minWidth,
maxWidth,
minHeight,
maxHeight,
attachmentPositions));
// Explicitly release smart pointers to free up space faster in JNI tables
componentNameRef.reset();
localDataMap.reset();
propsMap.reset();
return size;
}
TextLayoutManager::TextLayoutManager(
const ContextContainer::Shared& contextContainer)
: contextContainer_(contextContainer),
measureCache_(
CoreFeatures::cacheLastTextMeasurement
? 8096
: kSimpleThreadSafeCacheSizeCap) {}
void* TextLayoutManager::getNativeTextLayoutManager() const {
return self_;
}
TextMeasurement TextLayoutManager::measure(
const AttributedStringBox& attributedStringBox,
const ParagraphAttributes& paragraphAttributes,
const TextLayoutContext& layoutContext,
LayoutConstraints layoutConstraints,
std::shared_ptr<void> /* hostTextStorage */) const {
auto& attributedString = attributedStringBox.getValue();
auto measurement = measureCache_.get(
{attributedString, paragraphAttributes, layoutConstraints},
[&](const TextMeasureCacheKey& /*key*/) {
auto telemetry = TransactionTelemetry::threadLocalTelemetry();
if (telemetry != nullptr) {
telemetry->willMeasureText();
}
auto measurement = doMeasureMapBuffer(
attributedString, paragraphAttributes, layoutConstraints);
if (telemetry != nullptr) {
telemetry->didMeasureText();
}
return measurement;
});
measurement.size = layoutConstraints.clamp(measurement.size);
return measurement;
}
std::shared_ptr<void> TextLayoutManager::getHostTextStorage(
const AttributedString& /* attributedStringBox */,
const ParagraphAttributes& /* paragraphAttributes */,
LayoutConstraints /* layoutConstraints */) const {
return nullptr;
}
TextMeasurement TextLayoutManager::measureCachedSpannableById(
int64_t cacheId,
const ParagraphAttributes& paragraphAttributes,
LayoutConstraints layoutConstraints) const {
auto env = Environment::current();
auto attachmentPositions = env->NewFloatArray(0);
auto minimumSize = layoutConstraints.minimumSize;
auto maximumSize = layoutConstraints.maximumSize;
folly::dynamic cacheIdMap = folly::dynamic::object;
cacheIdMap["cacheId"] = cacheId;
auto size = measureAndroidComponent(
contextContainer_,
-1, // TODO: we should pass rootTag in
"RCTText",
std::move(cacheIdMap),
toDynamic(paragraphAttributes),
nullptr,
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, attachments};
}
LinesMeasurements TextLayoutManager::measureLines(
const AttributedString& attributedString,
const ParagraphAttributes& paragraphAttributes,
Size size) const {
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)>("measureLinesMapBuffer");
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.push_back(LineMeasurement(data));
}
// Explicitly release smart pointers to free up space faster in JNI tables
attributedStringMB.reset();
paragraphAttributesMB.reset();
return lineMeasurements;
}
TextMeasurement TextLayoutManager::doMeasure(
AttributedString attributedString,
const ParagraphAttributes& paragraphAttributes,
LayoutConstraints layoutConstraints) const {
layoutConstraints.maximumSize.height = std::numeric_limits<Float>::infinity();
const int attachmentCount = countAttachments(attributedString);
auto env = Environment::current();
auto attachmentPositions = env->NewFloatArray(attachmentCount * 2);
auto minimumSize = layoutConstraints.minimumSize;
auto maximumSize = layoutConstraints.maximumSize;
auto serializedAttributedString = toDynamic(attributedString);
auto size = measureAndroidComponent(
contextContainer_,
-1, // TODO: we should pass rootTag in
"RCTText",
serializedAttributedString,
toDynamic(paragraphAttributes),
nullptr,
minimumSize.width,
maximumSize.width,
minimumSize.height,
maximumSize.height,
attachmentPositions);
jfloat* attachmentData =
env->GetFloatArrayElements(attachmentPositions, nullptr);
auto attachments = TextMeasurement::Attachments{};
if (attachmentCount > 0) {
const folly::dynamic& fragments = serializedAttributedString["fragments"];
int attachmentIndex = 0;
for (const auto& fragment : fragments) {
auto isAttachment = fragment.find("isAttachment");
if (isAttachment != fragment.items().end() &&
isAttachment->second.getBool()) {
float top = attachmentData[attachmentIndex * 2];
float left = attachmentData[attachmentIndex * 2 + 1];
auto width = (float)fragment["width"].getDouble();
auto height = (float)fragment["height"].getDouble();
auto rect = facebook::react::Rect{
{left, top}, facebook::react::Size{width, height}};
attachments.push_back(TextMeasurement::Attachment{rect, false});
attachmentIndex++;
}
}
}
// Clean up allocated ref
env->ReleaseFloatArrayElements(
attachmentPositions, attachmentData, JNI_ABORT);
env->DeleteLocalRef(attachmentPositions);
return TextMeasurement{size, attachments};
}
TextMeasurement TextLayoutManager::doMeasureMapBuffer(
AttributedString attributedString,
const ParagraphAttributes& paragraphAttributes,
LayoutConstraints layoutConstraints) const {
layoutConstraints.maximumSize.height = std::numeric_limits<Float>::infinity();
const int attachmentCount = countAttachments(attributedString);
auto env = Environment::current();
auto attachmentPositions = env->NewFloatArray(attachmentCount * 2);
auto minimumSize = layoutConstraints.minimumSize;
auto maximumSize = layoutConstraints.maximumSize;
auto attributedStringMap = toMapBuffer(attributedString);
auto paragraphAttributesMap = toMapBuffer(paragraphAttributes);
auto size = measureAndroidComponentMapBuffer(
contextContainer_,
-1, // TODO: we should pass rootTag in
"RCTText",
std::move(attributedStringMap),
std::move(paragraphAttributesMap),
minimumSize.width,
maximumSize.width,
minimumSize.height,
maximumSize.height,
attachmentPositions);
jfloat* attachmentData =
env->GetFloatArrayElements(attachmentPositions, nullptr);
auto attachments = TextMeasurement::Attachments{};
if (attachmentCount > 0) {
int attachmentIndex = 0;
for (const auto& fragment : attributedString.getFragments()) {
if (fragment.isAttachment()) {
float top = attachmentData[attachmentIndex * 2];
float left = attachmentData[attachmentIndex * 2 + 1];
float width = fragment.parentShadowView.layoutMetrics.frame.size.width;
float height =
fragment.parentShadowView.layoutMetrics.frame.size.height;
auto rect = facebook::react::Rect{
{left, top}, facebook::react::Size{width, height}};
attachments.push_back(TextMeasurement::Attachment{rect, false});
attachmentIndex++;
}
}
}
// Clean up allocated ref
env->ReleaseFloatArrayElements(
attachmentPositions, attachmentData, JNI_ABORT);
env->DeleteLocalRef(attachmentPositions);
return TextMeasurement{size, attachments};
}
} // namespace facebook::react

View File

@@ -0,0 +1,103 @@
/*
* 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/config/ReactNativeConfig.h>
#include <react/renderer/attributedstring/AttributedString.h>
#include <react/renderer/attributedstring/AttributedStringBox.h>
#include <react/renderer/core/LayoutConstraints.h>
#include <react/renderer/textlayoutmanager/TextLayoutContext.h>
#include <react/renderer/textlayoutmanager/TextMeasureCache.h>
#include <react/utils/ContextContainer.h>
namespace facebook::react {
class TextLayoutManager;
using SharedTextLayoutManager = std::shared_ptr<const TextLayoutManager>;
/*
* Cross platform facade for Android-specific TextLayoutManager.
*/
class TextLayoutManager {
public:
TextLayoutManager(const ContextContainer::Shared& 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,
LayoutConstraints layoutConstraints,
std::shared_ptr<void> /* hostTextStorage */) const;
std::shared_ptr<void> getHostTextStorage(
const AttributedString& attributedString,
const ParagraphAttributes& paragraphAttributes,
LayoutConstraints layoutConstraints) const;
/**
* Measures an AttributedString on the platform, as identified by some
* opaque cache ID.
*/
TextMeasurement measureCachedSpannableById(
int64_t cacheId,
const ParagraphAttributes& paragraphAttributes,
LayoutConstraints layoutConstraints) const;
/*
* Measures lines of `attributedString` using native text rendering
* infrastructure.
*/
LinesMeasurements measureLines(
const AttributedString& attributedString,
const ParagraphAttributes& paragraphAttributes,
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.
*/
void* getNativeTextLayoutManager() const;
private:
TextMeasurement doMeasure(
AttributedString attributedString,
const ParagraphAttributes& paragraphAttributes,
LayoutConstraints layoutConstraints) const;
TextMeasurement doMeasureMapBuffer(
AttributedString attributedString,
const ParagraphAttributes& paragraphAttributes,
LayoutConstraints layoutConstraints) const;
LinesMeasurements measureLinesMapBuffer(
const AttributedString& attributedString,
const ParagraphAttributes& paragraphAttributes,
Size size) const;
void* self_{};
ContextContainer::Shared contextContainer_;
TextMeasureCache measureCache_;
};
} // namespace facebook::react

View File

@@ -0,0 +1,46 @@
/*
* 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 {
void* TextLayoutManager::getNativeTextLayoutManager() const {
return (void*)this;
}
TextMeasurement TextLayoutManager::measure(
AttributedStringBox attributedStringBox,
ParagraphAttributes paragraphAttributes,
const TextLayoutContext& /*layoutContext*/,
LayoutConstraints layoutConstraints,
std::shared_ptr<void>) const {
TextMeasurement::Attachments attachments;
for (const auto& fragment : attributedStringBox.getValue().getFragments()) {
if (fragment.isAttachment()) {
attachments.push_back(
TextMeasurement::Attachment{{{0, 0}, {0, 0}}, false});
}
}
return TextMeasurement{{0, 0}, attachments};
}
LinesMeasurements TextLayoutManager::measureLines(
AttributedString attributedString,
ParagraphAttributes paragraphAttributes,
Size size) const {
return {};
};
std::shared_ptr<void> TextLayoutManager::getHostTextStorage(
AttributedString attributedString,
ParagraphAttributes paragraphAttributes,
LayoutConstraints layoutConstraints) const {
return nullptr;
}
} // namespace facebook::react

View File

@@ -0,0 +1,66 @@
/*
* 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 <memory>
#include <react/renderer/attributedstring/AttributedString.h>
#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>
namespace facebook::react {
class TextLayoutManager;
using SharedTextLayoutManager = std::shared_ptr<const TextLayoutManager>;
/*
* Cross platform facade for Android-specific TextLayoutManager.
*/
class TextLayoutManager {
public:
TextLayoutManager(const ContextContainer::Shared& contextContainer) {}
virtual ~TextLayoutManager() = default;
/*
* Measures `attributedStringBox` using native text rendering infrastructure.
*/
virtual TextMeasurement measure(
AttributedStringBox attributedStringBox,
ParagraphAttributes paragraphAttributes,
const TextLayoutContext& layoutContext,
LayoutConstraints layoutConstraints,
std::shared_ptr<void>) const;
/*
* Measures lines of `attributedString` using native text rendering
* infrastructure.
*/
virtual LinesMeasurements measureLines(
AttributedString attributedString,
ParagraphAttributes paragraphAttributes,
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.
*/
void* getNativeTextLayoutManager() const;
virtual std::shared_ptr<void> getHostTextStorage(
AttributedString attributedStringBox,
ParagraphAttributes paragraphAttributes,
LayoutConstraints layoutConstraints) const;
};
} // namespace facebook::react

View File

@@ -0,0 +1,46 @@
/*
* 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`
*/
NSDictionary<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);
@interface RCTWeakEventEmitterWrapper : NSObject
@property (nonatomic, assign) facebook::react::SharedEventEmitter eventEmitter;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,464 @@
/*
* 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/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>
using namespace facebook::react;
@implementation RCTWeakEventEmitterWrapper {
std::weak_ptr<const EventEmitter> _weakEventEmitter;
}
- (void)setEventEmitter:(SharedEventEmitter)eventEmitter
{
_weakEventEmitter = eventEmitter;
}
- (SharedEventEmitter)eventEmitter
{
return _weakEventEmitter.lock();
}
- (void)dealloc
{
_weakEventEmitter.reset();
}
@end
inline static UIFontWeight RCTUIFontWeightFromInteger(NSInteger fontWeight)
{
assert(fontWeight > 50);
assert(fontWeight < 950);
static UIFontWeight weights[] = {
/* ~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)) {
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;
return [fontMetrics scaledValueForValue:requestedSize] / requestedSize;
} else {
return textAttributes.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];
}
NSDictionary<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 (!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};
shadow.shadowBlurRadius = textAttributes.textShadowRadius;
shadow.shadowColor = RCTUIColorFromSharedColor(textAttributes.textShadowColor);
attributes[NSShadowAttributeName] = shadow;
}
// Special
if (textAttributes.isHighlighted) {
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 copy];
}
static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText)
{
__block CGFloat maximumLineHeight = 0;
[attributedText enumerateAttribute:NSParagraphStyleAttributeName
inRange:NSMakeRange(0, attributedText.length)
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:NSMakeRange(0, attributedText.length)
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: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) {
RCTWeakEventEmitterWrapper *eventEmitterWrapper = [RCTWeakEventEmitterWrapper new];
eventEmitterWrapper.eventEmitter = 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];
}
RCTApplyBaselineOffset(nsAttributedString);
[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;
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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,
};
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,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.
*/
#import <UIKit/UIKit.h>
#import <react/renderer/textlayoutmanager/RCTFontProperties.h>
NS_ASSUME_NONNULL_BEGIN
/**
* Returns UIFont instance corresponded to given font properties.
*/
UIFont* RCTFontWithFontProperties(RCTFontProperties fontProperties);
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,167 @@
/*
* 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 <algorithm>
#import <cmath>
#import <limits>
#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 ? 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 UIFontWeight RCTGetFontWeight(UIFont *font)
{
NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute];
return [traits[UIFontWeightTrait] doubleValue];
}
static RCTFontStyle RCTGetFontStyle(UIFont *font)
{
NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute];
UIFontDescriptorSymbolicTraits symbolicTraits = [traits[UIFontSymbolicTrait] unsignedIntValue];
if (symbolicTraits & UIFontDescriptorTraitItalic) {
return RCTFontStyleItalic;
}
return RCTFontStyleNormal;
}
static NSArray *RCTFontFeatures(RCTFontVariant fontVariant)
{
// FIXME:
return @[];
}
static UIFont *RCTDefaultFontWithFontProperties(RCTFontProperties fontProperties)
{
static NSCache *fontCache;
static std::mutex fontCacheMutex;
CGFloat effectiveFontSize = fontProperties.sizeMultiplier * fontProperties.size;
NSString *cacheKey = [NSString stringWithFormat:@"%.1f/%.2f", effectiveFontSize, fontProperties.weight];
UIFont *font;
{
std::lock_guard<std::mutex> lock(fontCacheMutex);
if (!fontCache) {
fontCache = [NSCache new];
}
font = [fontCache objectForKey:cacheKey];
}
if (!font) {
font = [UIFont systemFontOfSize:effectiveFontSize weight:fontProperties.weight];
if (fontProperties.variant == RCTFontStyleItalic) {
UIFontDescriptor *fontDescriptor = [font fontDescriptor];
UIFontDescriptorSymbolicTraits symbolicTraits = fontDescriptor.symbolicTraits;
symbolicTraits |= UIFontDescriptorTraitItalic;
fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits];
font = [UIFont fontWithDescriptor:fontDescriptor size:effectiveFontSize];
}
{
std::lock_guard<std::mutex> lock(fontCacheMutex);
[fontCache setObject:font forKey:cacheKey];
}
}
return font;
}
UIFont *RCTFontWithFontProperties(RCTFontProperties fontProperties)
{
RCTFontProperties defaultFontProperties = RCTDefaultFontProperties();
fontProperties = RCTResolveFontProperties(fontProperties, defaultFontProperties);
assert(!isnan(fontProperties.sizeMultiplier));
CGFloat effectiveFontSize = fontProperties.sizeMultiplier * fontProperties.size;
UIFont *font;
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];
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) {
// Failback to system font.
font = [UIFont systemFontOfSize:effectiveFontSize weight:fontProperties.weight];
}
} else {
// 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 - fontProperties.weight) < ABS(closestWeight - fontProperties.weight)) {
font = fontMatch;
closestWeight = testWeight;
}
}
if (!font) {
// 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,67 @@
/*
* 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/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
layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints
textStorage:(NSTextStorage *_Nullable)textStorage;
- (facebook::react::TextMeasurement)measureNSAttributedString:(NSAttributedString *)attributedString
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints
textStorage:(NSTextStorage *_Nullable)textStorage;
- (NSTextStorage *)textStorageForAttributesString:(facebook::react::AttributedString)attributedString
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
size:(CGSize)size;
- (void)drawAttributedString:(facebook::react::AttributedString)attributedString
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
frame:(CGRect)frame
textStorage:(NSTextStorage *_Nullable)textStorage;
- (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,326 @@
/*
* 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/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
layoutConstraints:(LayoutConstraints)layoutConstraints
textStorage:(NSTextStorage *_Nullable)textStorage
{
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};
if (!textStorage) {
textStorage = [self _textStorageForNSAttributesString:attributedString
paragraphAttributes:paragraphAttributes
size:maximumSize];
}
return [self _measureTextStorage:textStorage];
}
- (TextMeasurement)measureAttributedString:(AttributedString)attributedString
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
layoutConstraints:(LayoutConstraints)layoutConstraints
textStorage:(NSTextStorage *_Nullable)textStorage
{
if (textStorage) {
return [self _measureTextStorage:textStorage];
} else {
return [self measureNSAttributedString:[self _nsAttributedStringFromAttributedString:attributedString]
paragraphAttributes:paragraphAttributes
layoutConstraints:layoutConstraints
textStorage:nil];
}
}
- (void)drawAttributedString:(AttributedString)attributedString
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
frame:(CGRect)frame
textStorage:(NSTextStorage *_Nullable)textStorage
{
BOOL createdStorageForFrame = NO;
if (!textStorage) {
textStorage = [self textStorageForAttributesString:attributedString
paragraphAttributes:paragraphAttributes
size:frame.size];
createdStorageForFrame = YES;
}
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
CGPoint origin = frame.origin;
if (!createdStorageForFrame) {
CGRect rect = [layoutManager usedRectForTextContainer:textContainer];
static auto threshold = 1.0 / RCTScreenScale() + 0.01; // Size of a pixel plus some small threshold.
// `rect`'s width is stored in double precesion.
// `frame`'s width is also in double precesion but was stored as float in Yoga previously, precesion was lost.
if (std::abs(RCTCeilPixelValue(rect.size.width) - frame.size.width) < threshold) {
// `textStorage` passed to this method was used to calculate size of frame. If that's the case, it's
// width is the same as frame's width. Origin must be adjusted, otherwise glyhps will be painted in wrong
// place.
// We could create new `NSTextStorage` for the specific frame, but that is expensive.
origin.x -= RCTCeilPixelValue(rect.origin.x);
}
}
#if TARGET_OS_MACCATALYST
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextSetShouldSmoothFonts(context, NO);
#endif
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:origin];
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:origin];
#if TARGET_OS_MACCATALYST
CGContextRestoreGState(context);
#endif
}
- (LinesMeasurements)getLinesForAttributedString:(AttributedString)attributedString
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
size:(CGSize)size
{
NSTextStorage *textStorage = [self textStorageForAttributesString: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{
facebook::react::Point{usedRect.origin.x, usedRect.origin.y},
facebook::react::Size{usedRect.size.width, usedRect.size.height}};
auto line = LineMeasurement{
std::string([renderedString UTF8String]),
rect,
-font.descender,
font.capHeight,
font.ascender,
font.xHeight};
blockParagraphLines->push_back(line);
}];
return paragraphLines;
}
- (NSTextStorage *)textStorageForAttributesString:(AttributedString)attributedString
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
size:(CGSize)size
{
return [self _textStorageForNSAttributesString:[self _nsAttributedStringFromAttributedString:attributedString]
paragraphAttributes:paragraphAttributes
size:size];
}
- (SharedEventEmitter)getEventEmitterWithAttributeString:(AttributedString)attributedString
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
frame:(CGRect)frame
atPoint:(CGPoint)point
{
NSTextStorage *textStorage = [self textStorageForAttributesString: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)) {
RCTWeakEventEmitterWrapper *eventEmitterWrapper =
(RCTWeakEventEmitterWrapper *)[textStorage attribute:RCTAttributedStringEventEmitterKey
atIndex:characterIndex
effectiveRange:NULL];
return eventEmitterWrapper.eventEmitter;
}
return nil;
}
- (void)getRectWithAttributedString:(AttributedString)attributedString
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
enumerateAttribute:(NSString *)enumerateAttribute
frame:(CGRect)frame
usingBlock:(RCTTextLayoutFragmentEnumerationBlock)block
{
NSTextStorage *textStorage = [self textStorageForAttributesString: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) {
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
- (NSTextStorage *)_textStorageForNSAttributesString:(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];
[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;
}
- (NSAttributedString *)_nsAttributedStringFromAttributedString:(AttributedString)attributedString
{
auto sharedNSAttributedString = _cache.get(attributedString, [](AttributedString attributedString) {
return wrapManagedObject(RCTNSAttributedStringFromAttributedString(attributedString));
});
return unwrapManagedObject(sharedNSAttributedString);
}
- (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage
{
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
[layoutManager ensureLayoutForTextContainer:textContainer];
CGSize size = [layoutManager usedRectForTextContainer:textContainer].size;
size = (CGSize){RCTCeilPixelValue(size.width), RCTCeilPixelValue(size.height)};
__block auto attachments = TextMeasurement::Attachments{};
[textStorage
enumerateAttribute:NSAttachmentAttributeName
inRange:NSMakeRange(0, textStorage.length)
options:0
usingBlock:^(NSTextAttachment *attachment, NSRange range, BOOL *stop) {
if (!attachment) {
return;
}
CGSize attachmentSize = attachment.bounds.size;
CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range inTextContainer:textContainer];
UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:range.location effectiveRange:nil];
CGRect frame = {
{glyphRect.origin.x,
glyphRect.origin.y + glyphRect.size.height - attachmentSize.height + font.descender},
attachmentSize};
auto rect = facebook::react::Rect{
facebook::react::Point{frame.origin.x, frame.origin.y},
facebook::react::Size{frame.size.width, frame.size.height}};
attachments.push_back(TextMeasurement::Attachment{rect, false});
}];
return TextMeasurement{{size.width, size.height}, attachments};
}
@end

View File

@@ -0,0 +1,102 @@
/*
* 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 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 NSUnderlinePatternDash | NSUnderlineStyleSingle;
case facebook::react::TextDecorationStyle::Dotted:
return NSUnderlinePatternDot | 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,65 @@
/*
* 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 <memory>
#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>
namespace facebook::react {
class TextLayoutManager;
/*
* Cross platform facade for iOS-specific RCTTTextLayoutManager.
*/
class TextLayoutManager {
public:
TextLayoutManager(const ContextContainer::Shared& contextContainer);
/*
* Measures `attributedString` using native text rendering infrastructure.
*/
TextMeasurement measure(
AttributedStringBox attributedStringBox,
ParagraphAttributes paragraphAttributes,
const TextLayoutContext& layoutContext,
LayoutConstraints layoutConstraints,
std::shared_ptr<void> hostTextStorage) const;
/*
* Measures lines of `attributedString` using native text rendering
* infrastructure.
*/
LinesMeasurements measureLines(
AttributedString attributedString,
ParagraphAttributes paragraphAttributes,
Size size) const;
std::shared_ptr<void> getHostTextStorage(
AttributedString attributedString,
ParagraphAttributes paragraphAttributes,
LayoutConstraints layoutConstraints) 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;
private:
std::shared_ptr<void> self_;
TextMeasureCache measureCache_{};
};
} // namespace facebook::react

View File

@@ -0,0 +1,119 @@
/*
* 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"
#include <react/renderer/telemetry/TransactionTelemetry.h>
#include <react/utils/ManagedObjectWrapper.h>
#import "RCTTextLayoutManager.h"
namespace facebook::react {
TextLayoutManager::TextLayoutManager(const ContextContainer::Shared &contextContainer)
{
self_ = wrapManagedObject([RCTTextLayoutManager new]);
}
std::shared_ptr<void> TextLayoutManager::getNativeTextLayoutManager() const
{
assert(self_ && "Stored NativeTextLayoutManager must not be null.");
return self_;
}
std::shared_ptr<void> TextLayoutManager::getHostTextStorage(
AttributedString attributedString,
ParagraphAttributes paragraphAttributes,
LayoutConstraints layoutConstraints) const
{
RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(self_);
CGSize maximumSize = CGSize{layoutConstraints.maximumSize.width, CGFLOAT_MAX};
NSTextStorage *textStorage = [textLayoutManager textStorageForAttributesString:attributedString
paragraphAttributes:paragraphAttributes
size:maximumSize];
return wrapManagedObject(textStorage);
}
TextMeasurement TextLayoutManager::measure(
AttributedStringBox attributedStringBox,
ParagraphAttributes paragraphAttributes,
const TextLayoutContext &layoutContext,
LayoutConstraints layoutConstraints,
std::shared_ptr<void> hostTextStorage) const
{
RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(self_);
NSTextStorage *textStorage;
if (hostTextStorage) {
textStorage = unwrapManagedObject(hostTextStorage);
}
auto measurement = TextMeasurement{};
switch (attributedStringBox.getMode()) {
case AttributedStringBox::Mode::Value: {
auto &attributedString = attributedStringBox.getValue();
measurement = measureCache_.get(
{attributedString, paragraphAttributes, layoutConstraints}, [&](const TextMeasureCacheKey &key) {
auto telemetry = TransactionTelemetry::threadLocalTelemetry();
if (telemetry) {
telemetry->willMeasureText();
}
auto measurement = [textLayoutManager measureAttributedString:attributedString
paragraphAttributes:paragraphAttributes
layoutConstraints:layoutConstraints
textStorage:textStorage];
if (telemetry) {
telemetry->didMeasureText();
}
return measurement;
});
break;
}
case AttributedStringBox::Mode::OpaquePointer: {
NSAttributedString *nsAttributedString =
(NSAttributedString *)unwrapManagedObject(attributedStringBox.getOpaquePointer());
auto telemetry = TransactionTelemetry::threadLocalTelemetry();
if (telemetry) {
telemetry->willMeasureText();
}
measurement = [textLayoutManager measureNSAttributedString:nsAttributedString
paragraphAttributes:paragraphAttributes
layoutConstraints:layoutConstraints
textStorage:textStorage];
if (telemetry) {
telemetry->didMeasureText();
}
break;
}
}
measurement.size = layoutConstraints.clamp(measurement.size);
return measurement;
}
LinesMeasurements TextLayoutManager::measureLines(
AttributedString attributedString,
ParagraphAttributes paragraphAttributes,
Size size) const
{
RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(self_);
return [textLayoutManager getLinesForAttributedString:attributedString
paragraphAttributes:paragraphAttributes
size:{size.width, size.height}];
}
} // 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:
}