This commit is contained in:
Yamozha
2021-04-02 02:24:13 +03:00
parent c23950b545
commit 7256d79e2c
31493 changed files with 3036630 additions and 0 deletions

View File

@ -0,0 +1,157 @@
load("@fbsource//tools/build_defs/apple:flag_defs.bzl", "get_preprocessor_flags_for_build_mode")
load(
"//tools/build_defs/oss:rn_defs.bzl",
"ANDROID",
"APPLE",
"CXX",
"YOGA_CXX_TARGET",
"fb_xplat_cxx_test",
"get_apple_compiler_flags",
"get_apple_inspector_flags",
"react_native_target",
"react_native_xplat_target",
"rn_xplat_cxx_library",
"subdir_glob",
)
APPLE_COMPILER_FLAGS = get_apple_compiler_flags()
rn_xplat_cxx_library(
name = "textlayoutmanager",
srcs = glob(
[
"*.cpp",
],
),
headers = subdir_glob(
[
("", "*.h"),
],
prefix = "",
),
header_namespace = "",
exported_headers = subdir_glob(
[
("", "*.h"),
],
prefix = "react/textlayoutmanager",
),
compiler_flags = [
"-fexceptions",
"-frtti",
"-std=c++14",
"-Wall",
],
cxx_exported_headers = subdir_glob(
[
("platform/cxx", "*.h"),
],
prefix = "react/textlayoutmanager",
),
cxx_headers = subdir_glob(
[
("platform/cxx", "**/*.h"),
],
prefix = "",
),
cxx_srcs = glob(
[
"platform/cxx/**/*.cpp",
],
),
cxx_tests = [":tests"],
fbandroid_deps = [
react_native_target("jni/react/jni:jni"),
],
fbandroid_exported_headers = subdir_glob(
[
("platform/android", "*.h"),
],
prefix = "react/textlayoutmanager",
),
fbandroid_headers = subdir_glob(
[
("platform/android", "**/*.h"),
],
prefix = "",
),
fbandroid_srcs = glob(
[
"platform/android/**/*.cpp",
],
),
fbobjc_compiler_flags = APPLE_COMPILER_FLAGS,
fbobjc_labels = ["supermodule:ios/default/public.react_native.infra"],
fbobjc_preprocessor_flags = get_preprocessor_flags_for_build_mode() + get_apple_inspector_flags(),
force_static = True,
ios_deps = [
],
ios_exported_headers = subdir_glob(
[
("platform/ios", "*.h"),
],
prefix = "react/textlayoutmanager",
),
ios_frameworks = [
"$SDKROOT/System/Library/Frameworks/CoreGraphics.framework",
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
],
ios_headers = subdir_glob(
[
("platform/ios", "**/*.h"),
],
prefix = "",
),
ios_srcs = glob(
[
"platform/ios/**/*.cpp",
"platform/ios/**/*.mm",
],
),
macosx_tests_override = [],
platforms = (ANDROID, APPLE, CXX),
preprocessor_flags = [
"-DLOG_TAG=\"ReactNative\"",
"-DWITH_FBSYSTRACE=1",
],
visibility = ["PUBLIC"],
deps = [
"//xplat/fbsystrace:fbsystrace",
"//xplat/folly:headers_only",
"//xplat/folly:memory",
"//xplat/folly:molly",
"//xplat/third-party/glog:glog",
YOGA_CXX_TARGET,
react_native_xplat_target("fabric/attributedstring:attributedstring"),
react_native_xplat_target("fabric/core:core"),
react_native_xplat_target("utils:utils"),
react_native_xplat_target("fabric/debug:debug"),
react_native_xplat_target("fabric/graphics:graphics"),
react_native_xplat_target("fabric/uimanager:uimanager"),
],
)
fb_xplat_cxx_test(
name = "tests",
srcs = glob(["tests/**/*.cpp"]),
headers = glob(["tests/**/*.h"]),
compiler_flags = [
"-fexceptions",
"-frtti",
"-std=c++14",
"-Wall",
],
contacts = ["oncall+react_native@xmail.facebook.com"],
platforms = (
# `Apple` and `Android` flavors are disabled because the module (built with those flavors) requires Emulator/Simulator (which is expensive and slow). At the same time, we don't really have tests here.
# ANDROID,
# APPLE,
CXX,
),
deps = [
":textlayoutmanager",
"//xplat/folly:molly",
"//xplat/third-party/gmock:gtest",
],
)

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "TextMeasureCache.h"
namespace facebook {
namespace react {} // namespace react
} // namespace facebook

View File

@ -0,0 +1,195 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <react/attributedstring/AttributedString.h>
#include <react/attributedstring/ParagraphAttributes.h>
#include <react/core/LayoutConstraints.h>
#include <react/utils/FloatComparison.h>
#include <react/utils/SimpleThreadSafeCache.h>
namespace facebook {
namespace react {
/*
* 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{256};
/*
* Thread-safe, evicting hash table designed to store text measurement
* information.
*/
using TextMeasureCache = SimpleThreadSafeCache<
TextMeasureCacheKey,
TextMeasurement,
kSimpleThreadSafeCacheSizeCap>;
inline bool areTextAttributesEquivalentLayoutWise(
TextAttributes const &lhs,
TextAttributes const &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.alignment) ==
std::tie(
rhs.fontFamily,
rhs.fontWeight,
rhs.fontStyle,
rhs.fontVariant,
rhs.allowFontScaling,
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(
TextAttributes const &textAttributes) {
// Taking into account the same props as
// `areTextAttributesEquivalentLayoutWise` mentions.
return folly::hash::hash_combine(
0,
textAttributes.fontFamily,
textAttributes.fontSize,
textAttributes.fontSizeMultiplier,
textAttributes.fontWeight,
textAttributes.fontStyle,
textAttributes.fontVariant,
textAttributes.allowFontScaling,
textAttributes.letterSpacing,
textAttributes.lineHeight,
textAttributes.alignment);
}
inline bool areAttributedStringFragmentsEquivalentLayoutWise(
AttributedString::Fragment const &lhs,
AttributedString::Fragment const &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 textAttributesHashLayoutWise(
AttributedString::Fragment const &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 folly::hash::hash_combine(
0,
fragment.string,
textAttributesHashLayoutWise(fragment.textAttributes));
}
inline bool areAttributedStringsEquivalentLayoutWise(
AttributedString const &lhs,
AttributedString const &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 textAttributedStringHashLayoutWise(
AttributedString const &attributedString) {
auto seed = size_t{0};
for (auto const &fragment : attributedString.getFragments()) {
seed =
folly::hash::hash_combine(seed, textAttributesHashLayoutWise(fragment));
}
return seed;
}
inline bool operator==(
TextMeasureCacheKey const &lhs,
TextMeasureCacheKey const &rhs) {
return areAttributedStringsEquivalentLayoutWise(
lhs.attributedString, rhs.attributedString) &&
lhs.paragraphAttributes == rhs.paragraphAttributes &&
lhs.layoutConstraints.maximumSize.width ==
rhs.layoutConstraints.maximumSize.width;
}
inline bool operator!=(
TextMeasureCacheKey const &lhs,
TextMeasureCacheKey const &rhs) {
return !(lhs == rhs);
}
} // namespace react
} // namespace facebook
namespace std {
template <>
struct hash<facebook::react::TextMeasureCacheKey> {
size_t operator()(facebook::react::TextMeasureCacheKey const &key) const {
return folly::hash::hash_combine(
0,
textAttributedStringHashLayoutWise(key.attributedString),
key.paragraphAttributes,
key.layoutConstraints.maximumSize.width);
}
};
} // namespace std

View File

@ -0,0 +1,123 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "TextLayoutManager.h"
#include <react/attributedstring/conversions.h>
#include <react/core/conversions.h>
#include <react/jni/ReadableNativeMap.h>
using namespace facebook::jni;
namespace facebook {
namespace react {
TextLayoutManager::~TextLayoutManager() {}
void *TextLayoutManager::getNativeTextLayoutManager() const {
return self_;
}
TextMeasurement TextLayoutManager::measure(
AttributedStringBox attributedStringBox,
ParagraphAttributes paragraphAttributes,
LayoutConstraints layoutConstraints) const {
auto &attributedString = attributedStringBox.getValue();
return measureCache_.get(
{attributedString, paragraphAttributes, layoutConstraints},
[&](TextMeasureCacheKey const &key) {
return doMeasure(
attributedString, paragraphAttributes, layoutConstraints);
});
}
TextMeasurement TextLayoutManager::doMeasure(
AttributedString attributedString,
ParagraphAttributes paragraphAttributes,
LayoutConstraints layoutConstraints) const {
const jni::global_ref<jobject> &fabricUIManager =
contextContainer_->at<jni::global_ref<jobject>>("FabricUIManager");
int attachmentsCount = 0;
for (auto fragment : attributedString.getFragments()) {
if (fragment.isAttachment()) {
attachmentsCount++;
}
}
auto env = Environment::current();
auto attachmentPositions = env->NewIntArray(attachmentsCount * 2);
static auto measure =
jni::findClassStatic("com/facebook/react/fabric/FabricUIManager")
->getMethod<jlong(
jint,
jstring,
ReadableMap::javaobject,
ReadableMap::javaobject,
ReadableMap::javaobject,
jfloat,
jfloat,
jfloat,
jfloat,
jintArray)>("measure");
auto minimumSize = layoutConstraints.minimumSize;
auto maximumSize = layoutConstraints.maximumSize;
auto serializedAttributedString = toDynamic(attributedString);
local_ref<JString> componentName = make_jstring("RCTText");
local_ref<ReadableNativeMap::javaobject> attributedStringRNM =
ReadableNativeMap::newObjectCxxArgs(serializedAttributedString);
local_ref<ReadableNativeMap::javaobject> paragraphAttributesRNM =
ReadableNativeMap::newObjectCxxArgs(toDynamic(paragraphAttributes));
local_ref<ReadableMap::javaobject> attributedStringRM = make_local(
reinterpret_cast<ReadableMap::javaobject>(attributedStringRNM.get()));
local_ref<ReadableMap::javaobject> paragraphAttributesRM = make_local(
reinterpret_cast<ReadableMap::javaobject>(paragraphAttributesRNM.get()));
auto size = yogaMeassureToSize(measure(
fabricUIManager,
-1,
componentName.get(),
attributedStringRM.get(),
paragraphAttributesRM.get(),
nullptr,
minimumSize.width,
maximumSize.width,
minimumSize.height,
maximumSize.height,
attachmentPositions));
jint *attachmentData = env->GetIntArrayElements(attachmentPositions, 0);
auto attachments = TextMeasurement::Attachments{};
if (attachmentsCount > 0) {
folly::dynamic fragments = serializedAttributedString["fragments"];
int attachmentIndex = 0;
for (int i = 0; i < fragments.size(); i++) {
folly::dynamic fragment = fragments[i];
if (fragment["isAttachment"] == true) {
float top = attachmentData[attachmentIndex * 2];
float left = attachmentData[attachmentIndex * 2 + 1];
float width = fragment["width"].getInt();
float height = fragment["height"].getInt();
auto rect = facebook::react::Rect{{left, top},
facebook::react::Size{width, height}};
attachments.push_back(TextMeasurement::Attachment{rect, false});
attachmentIndex++;
}
}
}
// DELETE REF
env->DeleteLocalRef(attachmentPositions);
return TextMeasurement{size, attachments};
}
} // namespace react
} // namespace facebook

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <react/attributedstring/AttributedString.h>
#include <react/attributedstring/AttributedStringBox.h>
#include <react/core/LayoutConstraints.h>
#include <react/textlayoutmanager/TextMeasureCache.h>
#include <react/utils/ContextContainer.h>
namespace facebook {
namespace 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)
: contextContainer_(contextContainer){};
~TextLayoutManager();
/*
* Measures `attributedString` using native text rendering infrastructure.
*/
TextMeasurement measure(
AttributedStringBox attributedStringBox,
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.
*/
void *getNativeTextLayoutManager() const;
private:
TextMeasurement doMeasure(
AttributedString attributedString,
ParagraphAttributes paragraphAttributes,
LayoutConstraints layoutConstraints) const;
void *self_;
ContextContainer::Shared contextContainer_;
TextMeasureCache measureCache_{};
};
} // namespace react
} // namespace facebook

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "TextLayoutManager.h"
namespace facebook {
namespace react {
TextLayoutManager::~TextLayoutManager() {}
void *TextLayoutManager::getNativeTextLayoutManager() const {
return self_;
}
Size TextLayoutManager::measure(
AttributedStringBox attributedStringBox,
ParagraphAttributes paragraphAttributes,
LayoutConstraints layoutConstraints) const {
return Size{0, 0};
}
} // namespace react
} // namespace facebook

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <memory>
#include <react/attributedstring/AttributedString.h>
#include <react/attributedstring/AttributedStringBox.h>
#include <react/attributedstring/ParagraphAttributes.h>
#include <react/core/LayoutConstraints.h>
#include <react/utils/ContextContainer.h>
namespace facebook {
namespace 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)
: contextContainer_(contextContainer){};
~TextLayoutManager();
/*
* Measures `attributedStringBox` using native text rendering infrastructure.
*/
Size measure(
AttributedStringBox attributedStringBox,
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.
*/
void *getNativeTextLayoutManager() const;
private:
void *self_;
ContextContainer::Shared contextContainer_;
};
} // namespace react
} // namespace facebook

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
@interface NSTextStorage (FontScaling)
- (void)scaleFontSizeToFitSize:(CGSize)size
minimumFontSize:(CGFloat)minimumFontSize
maximumFontSize:(CGFloat)maximumFontSize;
- (void)scaleFontSizeWithRatio:(CGFloat)ratio
minimumFontSize:(CGFloat)minimumFontSize
maximumFontSize:(CGFloat)maximumFontSize;
@end

View File

@ -0,0 +1,117 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "NSTextStorage+FontScaling.h"
typedef NS_OPTIONS(NSInteger, RCTTextSizeComparisonOptions) {
RCTTextSizeComparisonSmaller = 1 << 0,
RCTTextSizeComparisonLarger = 1 << 1,
RCTTextSizeComparisonWithinRange = 1 << 2,
};
@implementation NSTextStorage (FontScaling)
- (void)scaleFontSizeToFitSize:(CGSize)size
minimumFontSize:(CGFloat)minimumFontSize
maximumFontSize:(CGFloat)maximumFontSize
{
CGFloat bottomRatio = 1.0 / 128.0;
CGFloat topRatio = 128.0;
CGFloat ratio = 1.0;
NSAttributedString *originalAttributedString = [self copy];
CGFloat lastRatioWhichFits = 0.02;
while (true) {
[self scaleFontSizeWithRatio:ratio minimumFontSize:minimumFontSize maximumFontSize:maximumFontSize];
RCTTextSizeComparisonOptions comparsion = [self compareToSize:size thresholdRatio:0.01];
if ((comparsion & RCTTextSizeComparisonWithinRange) && (comparsion & RCTTextSizeComparisonSmaller)) {
return;
} else if (comparsion & RCTTextSizeComparisonSmaller) {
bottomRatio = ratio;
lastRatioWhichFits = ratio;
} else {
topRatio = ratio;
}
ratio = (topRatio + bottomRatio) / 2.0;
CGFloat kRatioThreshold = 0.005;
if (ABS(topRatio - bottomRatio) < kRatioThreshold || ABS(topRatio - ratio) < kRatioThreshold ||
ABS(bottomRatio - ratio) < kRatioThreshold) {
[self replaceCharactersInRange:(NSRange){0, self.length} withAttributedString:originalAttributedString];
[self scaleFontSizeWithRatio:lastRatioWhichFits minimumFontSize:minimumFontSize maximumFontSize:maximumFontSize];
return;
}
[self replaceCharactersInRange:(NSRange){0, self.length} withAttributedString:originalAttributedString];
}
}
- (RCTTextSizeComparisonOptions)compareToSize:(CGSize)size thresholdRatio:(CGFloat)thresholdRatio
{
NSLayoutManager *layoutManager = self.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
[layoutManager ensureLayoutForTextContainer:textContainer];
// Does it fit the text container?
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
NSRange truncatedGlyphRange = [layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:glyphRange.length - 1];
if (truncatedGlyphRange.location != NSNotFound) {
return RCTTextSizeComparisonLarger;
}
CGSize measuredSize = [layoutManager usedRectForTextContainer:textContainer].size;
// Does it fit the size?
BOOL fitsSize = size.width >= measuredSize.width && size.height >= measuredSize.height;
CGSize thresholdSize = (CGSize){
size.width * thresholdRatio,
size.height * thresholdRatio,
};
RCTTextSizeComparisonOptions result = 0;
result |= (fitsSize) ? RCTTextSizeComparisonSmaller : RCTTextSizeComparisonLarger;
if (ABS(measuredSize.width - size.width) < thresholdSize.width) {
result = result | RCTTextSizeComparisonWithinRange;
}
return result;
}
- (void)scaleFontSizeWithRatio:(CGFloat)ratio
minimumFontSize:(CGFloat)minimumFontSize
maximumFontSize:(CGFloat)maximumFontSize
{
[self beginEditing];
[self enumerateAttribute:NSFontAttributeName
inRange:(NSRange){0, self.length}
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
usingBlock:^(UIFont *_Nullable font, NSRange range, BOOL *_Nonnull stop) {
if (!font) {
return;
}
CGFloat fontSize = MAX(MIN(font.pointSize * ratio, maximumFontSize), minimumFontSize);
[self addAttribute:NSFontAttributeName value:[font fontWithSize:fontSize] range:range];
}];
[self endEditing];
}
@end

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#include <react/attributedstring/AttributedString.h>
#include <react/attributedstring/AttributedStringBox.h>
#include <react/attributedstring/TextAttributes.h>
NS_ASSUME_NONNULL_BEGIN
NSString *const RCTAttributedStringIsHighlightedAttributeName = @"IsHighlighted";
NSString *const RCTAttributedStringEventEmitterKey = @"EventEmitter";
/*
* Creates `NSTextAttributes` from given `facebook::react::TextAttributes`
*/
NSDictionary<NSAttributedStringKey, id> *RCTNSTextAttributesFromTextAttributes(
facebook::react::TextAttributes const &textAttributes);
/*
* Conversions amond `NSAttributedString`, `AttributedString` and `AttributedStringBox`.
*/
NSAttributedString *RCTNSAttributedStringFromAttributedString(
facebook::react::AttributedString const &attributedString);
NSAttributedString *RCTNSAttributedStringFromAttributedStringBox(
facebook::react::AttributedStringBox const &attributedStringBox);
facebook::react::AttributedStringBox RCTAttributedStringBoxFromNSAttributedString(
NSAttributedString *nsAttributedString);
@interface RCTWeakEventEmitterWrapper : NSObject
@property (nonatomic, assign) facebook::react::SharedEventEmitter eventEmitter;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,283 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTAttributedTextUtils.h"
#include <react/core/LayoutableShadowNode.h>
#include <react/textlayoutmanager/RCTFontProperties.h>
#include <react/textlayoutmanager/RCTFontUtils.h>
#include <react/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 UIFont *RCTEffectiveFontFromTextAttributes(const TextAttributes &textAttributes)
{
NSString *fontFamily = [NSString stringWithCString:textAttributes.fontFamily.c_str() encoding:NSUTF8StringEncoding];
RCTFontProperties fontProperties;
fontProperties.family = fontFamily;
fontProperties.size = textAttributes.fontSize;
fontProperties.style = textAttributes.fontStyle.hasValue()
? RCTFontStyleFromFontStyle(textAttributes.fontStyle.value())
: RCTFontStyleUndefined;
fontProperties.variant = textAttributes.fontVariant.hasValue()
? RCTFontVariantFromFontVariant(textAttributes.fontVariant.value())
: RCTFontVariantUndefined;
fontProperties.weight = textAttributes.fontWeight.hasValue()
? RCTUIFontWeightFromInteger((NSInteger)textAttributes.fontWeight.value())
: NAN;
fontProperties.sizeMultiplier = textAttributes.fontSizeMultiplier;
return RCTFontWithFontProperties(fontProperties);
}
inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const TextAttributes &textAttributes)
{
return textAttributes.allowFontScaling.value_or(true) && !isnan(textAttributes.fontSizeMultiplier)
? textAttributes.fontSizeMultiplier
: 1.0;
}
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(TextAttributes const &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.hasValue()) {
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.hasValue()) {
paragraphStyle.baseWritingDirection =
RCTNSWritingDirectionFromWritingDirection(textAttributes.baseWritingDirection.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 = RCTNSUnderlineStyleFromStyleAndPattern(
textAttributes.textDecorationLineStyle.value_or(TextDecorationLineStyle::Single),
textAttributes.textDecorationLinePattern.value_or(TextDecorationLinePattern::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.hasValue()) {
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;
}
return [attributes copy];
}
NSAttributedString *RCTNSAttributedStringFromAttributedString(const AttributedString &attributedString)
{
static UIImage *placeholderImage;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
placeholderImage = [[UIImage alloc] init];
});
NSMutableAttributedString *nsAttributedString = [[NSMutableAttributedString alloc] init];
[nsAttributedString beginEditing];
for (auto fragment : attributedString.getFragments()) {
NSMutableAttributedString *nsAttributedStringFragment;
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;
nsAttributedStringFragment = [[NSMutableAttributedString attributedStringWithAttachment:attachment] mutableCopy];
} else {
NSString *string = [NSString stringWithCString:fragment.string.c_str() encoding:NSUTF8StringEncoding];
nsAttributedStringFragment = [[NSMutableAttributedString alloc]
initWithString:string
attributes:RCTNSTextAttributesFromTextAttributes(fragment.textAttributes)];
}
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)];
}
[nsAttributedString appendAttributedString:nsAttributedStringFragment];
}
[nsAttributedString endEditing];
return nsAttributedString;
}
NSAttributedString *RCTNSAttributedStringFromAttributedStringBox(AttributedStringBox const &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{};
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#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) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <react/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,166 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#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;
NSString *cacheKey = [NSString stringWithFormat:@"%.1f/%.2f", fontProperties.size, 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:fontProperties.size 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:fontProperties.size];
}
{
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,43 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <react/attributedstring/AttributedString.h>
#import <react/attributedstring/ParagraphAttributes.h>
#import <react/core/LayoutConstraints.h>
#import <react/graphics/Geometry.h>
#import <react/textlayoutmanager/TextMeasureCache.h>
NS_ASSUME_NONNULL_BEGIN
/**
* iOS-specific TextLayoutManager
*/
@interface RCTTextLayoutManager : NSObject
- (facebook::react::TextMeasurement)measureAttributedString:(facebook::react::AttributedString)attributedString
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints;
- (facebook::react::TextMeasurement)measureNSAttributedString:(NSAttributedString *)attributedString
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
layoutConstraints:(facebook::react::LayoutConstraints)layoutConstraints;
- (void)drawAttributedString:(facebook::react::AttributedString)attributedString
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
frame:(CGRect)frame;
- (facebook::react::SharedEventEmitter)
getEventEmitterWithAttributeString:(facebook::react::AttributedString)attributedString
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
frame:(CGRect)frame
atPoint:(CGPoint)point;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,182 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTTextLayoutManager.h"
#import "NSTextStorage+FontScaling.h"
#import "RCTAttributedTextUtils.h"
#import <react/utils/SimpleThreadSafeCache.h>
using namespace facebook::react;
@implementation RCTTextLayoutManager {
SimpleThreadSafeCache<AttributedString, std::shared_ptr<const 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
{
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];
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
[layoutManager ensureLayoutForTextContainer:textContainer];
CGSize size = [layoutManager usedRectForTextContainer:textContainer].size;
__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};
}
- (TextMeasurement)measureAttributedString:(AttributedString)attributedString
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
layoutConstraints:(LayoutConstraints)layoutConstraints
{
return [self measureNSAttributedString:[self _nsAttributedStringFromAttributedString:attributedString]
paragraphAttributes:paragraphAttributes
layoutConstraints:layoutConstraints];
}
- (void)drawAttributedString:(AttributedString)attributedString
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
frame:(CGRect)frame
{
NSTextStorage *textStorage = [self
_textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString]
paragraphAttributes:paragraphAttributes
size:frame.size];
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:frame.origin];
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:frame.origin];
}
- (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];
[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)) {
RCTWeakEventEmitterWrapper *eventEmitterWrapper =
(RCTWeakEventEmitterWrapper *)[textStorage attribute:RCTAttributedStringEventEmitterKey
atIndex:characterIndex
effectiveRange:NULL];
return eventEmitterWrapper.eventEmitter;
}
return nil;
}
- (NSAttributedString *)_nsAttributedStringFromAttributedString:(AttributedString)attributedString
{
auto sharedNSAttributedString = _cache.get(attributedString, [](const AttributedString attributedString) {
return std::shared_ptr<void>(
(__bridge_retained void *)RCTNSAttributedStringFromAttributedString(attributedString), CFRelease);
});
return (__bridge NSAttributedString *)sharedNSAttributedString.get();
}
@end

View File

@ -0,0 +1,102 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#include <react/textlayoutmanager/RCTFontProperties.h>
#include <react/textlayoutmanager/RCTFontUtils.h>
using namespace facebook::react;
inline static NSTextAlignment RCTNSTextAlignmentFromTextAlignment(TextAlignment textAlignment)
{
switch (textAlignment) {
case TextAlignment::Natural:
return NSTextAlignmentNatural;
case TextAlignment::Left:
return NSTextAlignmentLeft;
case TextAlignment::Right:
return NSTextAlignmentRight;
case TextAlignment::Center:
return NSTextAlignmentCenter;
case TextAlignment::Justified:
return NSTextAlignmentJustified;
}
}
inline static NSWritingDirection RCTNSWritingDirectionFromWritingDirection(WritingDirection writingDirection)
{
switch (writingDirection) {
case WritingDirection::Natural:
return NSWritingDirectionNatural;
case WritingDirection::LeftToRight:
return NSWritingDirectionLeftToRight;
case WritingDirection::RightToLeft:
return NSWritingDirectionRightToLeft;
}
}
inline static RCTFontStyle RCTFontStyleFromFontStyle(FontStyle fontStyle)
{
switch (fontStyle) {
case FontStyle::Normal:
return RCTFontStyleNormal;
case FontStyle::Italic:
return RCTFontStyleItalic;
case FontStyle::Oblique:
return RCTFontStyleOblique;
}
}
inline static RCTFontVariant RCTFontVariantFromFontVariant(FontVariant fontVariant)
{
return (RCTFontVariant)fontVariant;
}
inline static NSUnderlineStyle RCTNSUnderlineStyleFromStyleAndPattern(
TextDecorationLineStyle textDecorationLineStyle,
TextDecorationLinePattern textDecorationLinePattern)
{
NSUnderlineStyle style = NSUnderlineStyleNone;
switch (textDecorationLineStyle) {
case TextDecorationLineStyle::Single:
style = NSUnderlineStyle(style | NSUnderlineStyleSingle);
break;
case TextDecorationLineStyle::Thick:
style = NSUnderlineStyle(style | NSUnderlineStyleThick);
break;
case TextDecorationLineStyle::Double:
style = NSUnderlineStyle(style | NSUnderlineStyleDouble);
break;
}
switch (textDecorationLinePattern) {
case TextDecorationLinePattern::Solid:
style = NSUnderlineStyle(style | NSUnderlinePatternSolid);
break;
case TextDecorationLinePattern::Dash:
style = NSUnderlineStyle(style | NSUnderlinePatternDash);
break;
case TextDecorationLinePattern::Dot:
style = NSUnderlineStyle(style | NSUnderlinePatternDot);
break;
case TextDecorationLinePattern::DashDot:
style = NSUnderlineStyle(style | NSUnderlinePatternDashDot);
break;
case TextDecorationLinePattern::DashDotDot:
style = NSUnderlineStyle(style | NSUnderlinePatternDashDotDot);
break;
}
return style;
}
inline static UIColor *RCTUIColorFromSharedColor(const SharedColor &color)
{
return color ? [UIColor colorWithCGColor:color.get()] : nil;
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <memory>
#include <react/attributedstring/AttributedStringBox.h>
#include <react/attributedstring/ParagraphAttributes.h>
#include <react/core/LayoutConstraints.h>
#include <react/textlayoutmanager/TextMeasureCache.h>
#include <react/utils/ContextContainer.h>
namespace facebook {
namespace react {
class TextLayoutManager;
using SharedTextLayoutManager = std::shared_ptr<const TextLayoutManager>;
/*
* Cross platform facade for iOS-specific RCTTTextLayoutManager.
*/
class TextLayoutManager {
public:
using Shared = std::shared_ptr<TextLayoutManager const>;
TextLayoutManager(ContextContainer::Shared const &contextContainer);
/*
* Measures `attributedString` using native text rendering infrastructure.
*/
TextMeasurement measure(
AttributedStringBox attributedStringBox,
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 react
} // namespace facebook

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "TextLayoutManager.h"
#include <react/utils/ManagedObjectWrapper.h>
#import "RCTTextLayoutManager.h"
namespace facebook {
namespace react {
TextLayoutManager::TextLayoutManager(ContextContainer::Shared const &contextContainer)
{
self_ = wrapManagedObject([RCTTextLayoutManager new]);
}
std::shared_ptr<void> TextLayoutManager::getNativeTextLayoutManager() const
{
assert(self_ && "Stored NativeTextLayoutManager must not be null.");
return self_;
}
TextMeasurement TextLayoutManager::measure(
AttributedStringBox attributedStringBox,
ParagraphAttributes paragraphAttributes,
LayoutConstraints layoutConstraints) const
{
RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(self_);
auto measurement = TextMeasurement{};
switch (attributedStringBox.getMode()) {
case AttributedStringBox::Mode::Value: {
auto &attributedString = attributedStringBox.getValue();
measurement = measureCache_.get(
{attributedString, paragraphAttributes, layoutConstraints}, [&](TextMeasureCacheKey const &key) {
return [textLayoutManager measureAttributedString:attributedString
paragraphAttributes:paragraphAttributes
layoutConstraints:layoutConstraints];
});
break;
}
case AttributedStringBox::Mode::OpaquePointer: {
NSAttributedString *nsAttributedString =
(NSAttributedString *)unwrapManagedObject(attributedStringBox.getOpaquePointer());
measurement = [textLayoutManager measureNSAttributedString:nsAttributedString
paragraphAttributes:paragraphAttributes
layoutConstraints:layoutConstraints];
break;
}
}
measurement.size = layoutConstraints.clamp(measurement.size);
return measurement;
}
} // namespace react
} // namespace facebook

View File

@ -0,0 +1,18 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <memory>
#include <gtest/gtest.h>
#include <react/textlayoutmanager/TextLayoutManager.h>
using namespace facebook::react;
TEST(TextLayoutManagerTest, testSomething) {
// TODO:
}