mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
db284ae037
Summary:
<!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? -->
Few month ago, I created a [pull request](https://github.com/facebook/react-native/pull/32774) to unify the behavior of capitalize style between Android and IOS.
But, I found out that it doesn't work when fabric is enabled. We have the old behavior :
| Android | IOS |
| ------------- | ------------- |
| <img width="458" alt="capitalize_android_fabric" src="https://user-images.githubusercontent.com/40902940/163182082-4061003c-230b-46f7-9e93-c34b66dbf3d2.png"> | <img width="476" alt="capitalize_ios_fabric" src="https://user-images.githubusercontent.com/40902940/163182124-b6dee450-46e3-41a3-b5bb-553d7c2662e6.png"> |
(source: rn-tester, last commit: dac56ce)
As fabric is now live since v0.68, we should fix this behavior for fabric aswell.
I don't know why there is so much duplicated code between `ReactCommon/react/renderer/textlayoutmanager/platform/ios/RCTAttributedTextUtils.mm` and `Libraries/Text/RCTTextAttributes.m` because I don't know the architecture of the project very well. But if you see missing tests or some refacto to do I'm open to suggestions!
## Changelog
<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://github.com/facebook/react-native/wiki/Changelog
-->
[iOS] [Fixed] - Don't capitalize the first letter of a word that is starting by a number (Fabric renderer)
Pull Request resolved: https://github.com/facebook/react-native/pull/33629
Test Plan:
I manually tested these changes on rn-tester (react-native `main` branch):
| Fabric ? | Android | IOS |
| ------------- | ------------- | ------------- |
| YES | <img width="458" alt="capitalize_android_fabric" src="https://user-images.githubusercontent.com/40902940/163182082-4061003c-230b-46f7-9e93-c34b66dbf3d2.png"> | <img width="476" alt="capitalize_ios_fabric_after" src="https://user-images.githubusercontent.com/40902940/163184277-6c58c117-7144-4f6b-98ea-0c1db654f27b.png"> |
| NO | <img width="458" alt="android_nf_after" src="https://user-images.githubusercontent.com/40902940/163190263-9d801f6a-09c2-4ec6-a841-3dca115a5ef7.png"> | <img width="476" alt="ios_nf_after" src="https://user-images.githubusercontent.com/40902940/163190333-7e9eac6a-3f28-4826-8ef9-bcf45bf870a9.png"> |
Reviewed By: cortinico
Differential Revision: D35611086
Pulled By: GijsWeterings
fbshipit-source-id: 0c43807dcddb30e65921eb1525c0fe440162ec32
411 lines
16 KiB
Plaintext
411 lines
16 KiB
Plaintext
/*
|
|
* 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/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 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.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 = 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.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 (!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.accessibilityRole.has_value()) {
|
|
auto accessibilityRole = textAttributes.accessibilityRole.value();
|
|
switch (accessibilityRole) {
|
|
case AccessibilityRole::None:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("none");
|
|
break;
|
|
case AccessibilityRole::Button:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("button");
|
|
break;
|
|
case AccessibilityRole::Link:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("link");
|
|
break;
|
|
case AccessibilityRole::Search:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("search");
|
|
break;
|
|
case AccessibilityRole::Image:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("image");
|
|
break;
|
|
case AccessibilityRole::Imagebutton:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("imagebutton");
|
|
break;
|
|
case AccessibilityRole::Keyboardkey:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("keyboardkey");
|
|
break;
|
|
case AccessibilityRole::Text:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("text");
|
|
break;
|
|
case AccessibilityRole::Adjustable:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("adjustable");
|
|
break;
|
|
case AccessibilityRole::Summary:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("summary");
|
|
break;
|
|
case AccessibilityRole::Header:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("header");
|
|
break;
|
|
case AccessibilityRole::Alert:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("alert");
|
|
break;
|
|
case AccessibilityRole::Checkbox:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("checkbox");
|
|
break;
|
|
case AccessibilityRole::Combobox:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("combobox");
|
|
break;
|
|
case AccessibilityRole::Menu:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("menu");
|
|
break;
|
|
case AccessibilityRole::Menubar:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("menubar");
|
|
break;
|
|
case AccessibilityRole::Menuitem:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("menuitem");
|
|
break;
|
|
case AccessibilityRole::Progressbar:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("progressbar");
|
|
break;
|
|
case AccessibilityRole::Radio:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("radio");
|
|
break;
|
|
case AccessibilityRole::Radiogroup:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("radiogroup");
|
|
break;
|
|
case AccessibilityRole::Scrollbar:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("scrollbar");
|
|
break;
|
|
case AccessibilityRole::Spinbutton:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("spinbutton");
|
|
break;
|
|
case AccessibilityRole::Switch:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("switch");
|
|
break;
|
|
case AccessibilityRole::Tab:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("tab");
|
|
break;
|
|
case AccessibilityRole::TabBar:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("tabbar");
|
|
break;
|
|
case AccessibilityRole::Tablist:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("tablist");
|
|
break;
|
|
case AccessibilityRole::Timer:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("timer");
|
|
break;
|
|
case AccessibilityRole::Toolbar:
|
|
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = @("toolbar");
|
|
break;
|
|
};
|
|
}
|
|
|
|
return [attributes copy];
|
|
}
|
|
|
|
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;
|
|
|
|
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];
|
|
|
|
if (fragment.textAttributes.textTransform.has_value()) {
|
|
auto textTransform = fragment.textAttributes.textTransform.value();
|
|
string = RCTNSStringFromStringApplyingTextTransform(string, textTransform);
|
|
}
|
|
|
|
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{};
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|