mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
89efa1a0c1
Summary: Before https://github.com/facebook/react-native/commit/f951da912dd8b4dc2b0d674f8f37f9d982a03c48 finding a system font used to return early. In order to allow variants, the referenced patch removed the early return so that variants could be applied later. However, there is no need to find the closest font as we already selected the proper system font. This also fixes a bug with setting a custom font handler via RCTSetDefaultFontHandler whos return could get overwritten by the closest font search. ## 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] - Respect RCTSetDefaultFontHandler chosen font Pull Request resolved: https://github.com/facebook/react-native/pull/32482 Reviewed By: ShikaSD Differential Revision: D33844138 Pulled By: cortinico fbshipit-source-id: 05c01fc358cd19f8be342218cdba944b303073ed
412 lines
13 KiB
Plaintext
412 lines
13 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 "RCTFont.h"
|
|
#import "RCTAssert.h"
|
|
#import "RCTLog.h"
|
|
|
|
#import <CoreText/CoreText.h>
|
|
|
|
typedef CGFloat RCTFontWeight;
|
|
static RCTFontWeight weightOfFont(UIFont *font)
|
|
{
|
|
static NSArray<NSString *> *weightSuffixes;
|
|
static NSArray<NSNumber *> *fontWeights;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
// We use two arrays instead of one map because
|
|
// the order is important for suffix matching.
|
|
weightSuffixes = @[
|
|
@"normal",
|
|
@"ultralight",
|
|
@"thin",
|
|
@"light",
|
|
@"regular",
|
|
@"medium",
|
|
@"semibold",
|
|
@"demibold",
|
|
@"extrabold",
|
|
@"ultrabold",
|
|
@"bold",
|
|
@"heavy",
|
|
@"black"
|
|
];
|
|
fontWeights = @[
|
|
@(UIFontWeightRegular),
|
|
@(UIFontWeightUltraLight),
|
|
@(UIFontWeightThin),
|
|
@(UIFontWeightLight),
|
|
@(UIFontWeightRegular),
|
|
@(UIFontWeightMedium),
|
|
@(UIFontWeightSemibold),
|
|
@(UIFontWeightSemibold),
|
|
@(UIFontWeightHeavy),
|
|
@(UIFontWeightHeavy),
|
|
@(UIFontWeightBold),
|
|
@(UIFontWeightHeavy),
|
|
@(UIFontWeightBlack)
|
|
];
|
|
});
|
|
|
|
NSString *fontName = font.fontName;
|
|
NSInteger i = 0;
|
|
for (NSString *suffix in weightSuffixes) {
|
|
// CFStringFind is much faster than any variant of rangeOfString: because it does not use a locale.
|
|
auto options = kCFCompareCaseInsensitive | kCFCompareAnchored | kCFCompareBackwards;
|
|
if (CFStringFind((CFStringRef)fontName, (CFStringRef)suffix, options).location != kCFNotFound) {
|
|
return (RCTFontWeight)fontWeights[i].doubleValue;
|
|
}
|
|
i++;
|
|
}
|
|
|
|
auto traits = (__bridge_transfer NSDictionary *)CTFontCopyTraits((CTFontRef)font);
|
|
return (RCTFontWeight)[traits[UIFontWeightTrait] doubleValue];
|
|
}
|
|
|
|
static BOOL isItalicFont(UIFont *font)
|
|
{
|
|
return (CTFontGetSymbolicTraits((CTFontRef)font) & kCTFontTraitItalic) != 0;
|
|
}
|
|
|
|
static BOOL isCondensedFont(UIFont *font)
|
|
{
|
|
return (CTFontGetSymbolicTraits((CTFontRef)font) & kCTFontTraitCondensed) != 0;
|
|
}
|
|
|
|
static RCTFontHandler defaultFontHandler;
|
|
|
|
void RCTSetDefaultFontHandler(RCTFontHandler handler)
|
|
{
|
|
defaultFontHandler = handler;
|
|
}
|
|
|
|
BOOL RCTHasFontHandlerSet()
|
|
{
|
|
return defaultFontHandler != nil;
|
|
}
|
|
|
|
// We pass a string description of the font weight to the defaultFontHandler because UIFontWeight
|
|
// is not defined pre-iOS 8.2.
|
|
// Furthermore, UIFontWeight's are lossy floats, so we must use an inexact compare to figure out
|
|
// which one we actually have.
|
|
static inline BOOL CompareFontWeights(UIFontWeight firstWeight, UIFontWeight secondWeight)
|
|
{
|
|
#if CGFLOAT_IS_DOUBLE
|
|
return fabs(firstWeight - secondWeight) < 0.01;
|
|
#else
|
|
return fabsf(firstWeight - secondWeight) < 0.01;
|
|
#endif
|
|
}
|
|
|
|
static NSString *FontWeightDescriptionFromUIFontWeight(UIFontWeight fontWeight)
|
|
{
|
|
if (CompareFontWeights(fontWeight, UIFontWeightUltraLight)) {
|
|
return @"ultralight";
|
|
} else if (CompareFontWeights(fontWeight, UIFontWeightThin)) {
|
|
return @"thin";
|
|
} else if (CompareFontWeights(fontWeight, UIFontWeightLight)) {
|
|
return @"light";
|
|
} else if (CompareFontWeights(fontWeight, UIFontWeightRegular)) {
|
|
return @"regular";
|
|
} else if (CompareFontWeights(fontWeight, UIFontWeightMedium)) {
|
|
return @"medium";
|
|
} else if (CompareFontWeights(fontWeight, UIFontWeightSemibold)) {
|
|
return @"semibold";
|
|
} else if (CompareFontWeights(fontWeight, UIFontWeightBold)) {
|
|
return @"bold";
|
|
} else if (CompareFontWeights(fontWeight, UIFontWeightHeavy)) {
|
|
return @"heavy";
|
|
} else if (CompareFontWeights(fontWeight, UIFontWeightBlack)) {
|
|
return @"black";
|
|
}
|
|
RCTAssert(NO, @"Unknown UIFontWeight passed in: %f", fontWeight);
|
|
return @"regular";
|
|
}
|
|
|
|
static UIFont *cachedSystemFont(CGFloat size, RCTFontWeight weight)
|
|
{
|
|
static NSCache<NSValue *, UIFont *> *fontCache = [NSCache new];
|
|
|
|
struct __attribute__((__packed__)) CacheKey {
|
|
CGFloat size;
|
|
RCTFontWeight weight;
|
|
};
|
|
|
|
CacheKey key{size, weight};
|
|
NSValue *cacheKey = [[NSValue alloc] initWithBytes:&key objCType:@encode(CacheKey)];
|
|
UIFont *font = [fontCache objectForKey:cacheKey];
|
|
|
|
if (!font) {
|
|
if (defaultFontHandler) {
|
|
NSString *fontWeightDescription = FontWeightDescriptionFromUIFontWeight(weight);
|
|
font = defaultFontHandler(size, fontWeightDescription);
|
|
} else {
|
|
font = [UIFont systemFontOfSize:size weight:weight];
|
|
}
|
|
|
|
[fontCache setObject:font forKey:cacheKey];
|
|
}
|
|
|
|
return font;
|
|
}
|
|
|
|
// Caching wrapper around expensive +[UIFont fontNamesForFamilyName:]
|
|
static NSArray<NSString *> *fontNamesForFamilyName(NSString *familyName)
|
|
{
|
|
static NSCache<NSString *, NSArray<NSString *> *> *cache;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
cache = [NSCache new];
|
|
[NSNotificationCenter.defaultCenter
|
|
addObserverForName:(NSNotificationName)kCTFontManagerRegisteredFontsChangedNotification
|
|
object:nil
|
|
queue:nil
|
|
usingBlock:^(NSNotification *) {
|
|
[cache removeAllObjects];
|
|
}];
|
|
});
|
|
|
|
auto names = [cache objectForKey:familyName];
|
|
if (!names) {
|
|
names = [UIFont fontNamesForFamilyName:familyName] ?: [NSArray new];
|
|
[cache setObject:names forKey:familyName];
|
|
}
|
|
return names;
|
|
}
|
|
|
|
@implementation RCTConvert (RCTFont)
|
|
|
|
+ (UIFont *)UIFont:(id)json
|
|
{
|
|
json = [self NSDictionary:json];
|
|
return [RCTFont updateFont:nil
|
|
withFamily:[RCTConvert NSString:json[@"fontFamily"]]
|
|
size:[RCTConvert NSNumber:json[@"fontSize"]]
|
|
weight:[RCTConvert NSString:json[@"fontWeight"]]
|
|
style:[RCTConvert NSString:json[@"fontStyle"]]
|
|
variant:[RCTConvert NSStringArray:json[@"fontVariant"]]
|
|
scaleMultiplier:1];
|
|
}
|
|
|
|
RCT_ENUM_CONVERTER(
|
|
RCTFontWeight,
|
|
(@{
|
|
@"normal" : @(UIFontWeightRegular),
|
|
@"bold" : @(UIFontWeightBold),
|
|
@"100" : @(UIFontWeightUltraLight),
|
|
@"200" : @(UIFontWeightThin),
|
|
@"300" : @(UIFontWeightLight),
|
|
@"400" : @(UIFontWeightRegular),
|
|
@"500" : @(UIFontWeightMedium),
|
|
@"600" : @(UIFontWeightSemibold),
|
|
@"700" : @(UIFontWeightBold),
|
|
@"800" : @(UIFontWeightHeavy),
|
|
@"900" : @(UIFontWeightBlack),
|
|
}),
|
|
UIFontWeightRegular,
|
|
doubleValue)
|
|
|
|
typedef BOOL RCTFontStyle;
|
|
RCT_ENUM_CONVERTER(
|
|
RCTFontStyle,
|
|
(@{
|
|
@"normal" : @NO,
|
|
@"italic" : @YES,
|
|
@"oblique" : @YES,
|
|
}),
|
|
NO,
|
|
boolValue)
|
|
|
|
typedef NSDictionary RCTFontVariantDescriptor;
|
|
+ (RCTFontVariantDescriptor *)RCTFontVariantDescriptor:(id)json
|
|
{
|
|
static NSDictionary *mapping;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
mapping = @{
|
|
@"small-caps" : @{
|
|
UIFontFeatureTypeIdentifierKey : @(kLowerCaseType),
|
|
UIFontFeatureSelectorIdentifierKey : @(kLowerCaseSmallCapsSelector),
|
|
},
|
|
@"oldstyle-nums" : @{
|
|
UIFontFeatureTypeIdentifierKey : @(kNumberCaseType),
|
|
UIFontFeatureSelectorIdentifierKey : @(kLowerCaseNumbersSelector),
|
|
},
|
|
@"lining-nums" : @{
|
|
UIFontFeatureTypeIdentifierKey : @(kNumberCaseType),
|
|
UIFontFeatureSelectorIdentifierKey : @(kUpperCaseNumbersSelector),
|
|
},
|
|
@"tabular-nums" : @{
|
|
UIFontFeatureTypeIdentifierKey : @(kNumberSpacingType),
|
|
UIFontFeatureSelectorIdentifierKey : @(kMonospacedNumbersSelector),
|
|
},
|
|
@"proportional-nums" : @{
|
|
UIFontFeatureTypeIdentifierKey : @(kNumberSpacingType),
|
|
UIFontFeatureSelectorIdentifierKey : @(kProportionalNumbersSelector),
|
|
},
|
|
};
|
|
});
|
|
RCTFontVariantDescriptor *value = mapping[json];
|
|
if (RCT_DEBUG && !value && [json description].length > 0) {
|
|
RCTLogError(
|
|
@"Invalid RCTFontVariantDescriptor '%@'. should be one of: %@",
|
|
json,
|
|
[[mapping allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
RCT_ARRAY_CONVERTER(RCTFontVariantDescriptor)
|
|
|
|
@end
|
|
|
|
@implementation RCTFont
|
|
|
|
+ (UIFont *)updateFont:(UIFont *)font
|
|
withFamily:(NSString *)family
|
|
size:(NSNumber *)size
|
|
weight:(NSString *)weight
|
|
style:(NSString *)style
|
|
variant:(NSArray<RCTFontVariantDescriptor *> *)variant
|
|
scaleMultiplier:(CGFloat)scaleMultiplier
|
|
{
|
|
// Defaults
|
|
static NSString *defaultFontFamily;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
defaultFontFamily = [UIFont systemFontOfSize:14].familyName;
|
|
});
|
|
const RCTFontWeight defaultFontWeight = UIFontWeightRegular;
|
|
const CGFloat defaultFontSize = 14;
|
|
|
|
// Initialize properties to defaults
|
|
CGFloat fontSize = defaultFontSize;
|
|
RCTFontWeight fontWeight = defaultFontWeight;
|
|
NSString *familyName = defaultFontFamily;
|
|
BOOL isItalic = NO;
|
|
BOOL isCondensed = NO;
|
|
|
|
if (font) {
|
|
familyName = font.familyName ?: defaultFontFamily;
|
|
fontSize = font.pointSize ?: defaultFontSize;
|
|
fontWeight = weightOfFont(font);
|
|
isItalic = isItalicFont(font);
|
|
isCondensed = isCondensedFont(font);
|
|
}
|
|
|
|
// Get font attributes
|
|
fontSize = [RCTConvert CGFloat:size] ?: fontSize;
|
|
if (scaleMultiplier > 0.0 && scaleMultiplier != 1.0) {
|
|
fontSize = round(fontSize * scaleMultiplier);
|
|
}
|
|
familyName = [RCTConvert NSString:family] ?: familyName;
|
|
isItalic = style ? [RCTConvert RCTFontStyle:style] : isItalic;
|
|
fontWeight = weight ? [RCTConvert RCTFontWeight:weight] : fontWeight;
|
|
|
|
BOOL didFindFont = NO;
|
|
|
|
// Handle system font as special case. This ensures that we preserve
|
|
// the specific metrics of the standard system font as closely as possible.
|
|
if ([familyName isEqual:defaultFontFamily] || [familyName isEqualToString:@"System"]) {
|
|
font = cachedSystemFont(fontSize, fontWeight);
|
|
if (font) {
|
|
didFindFont = YES;
|
|
|
|
if (isItalic || isCondensed) {
|
|
UIFontDescriptor *fontDescriptor = [font fontDescriptor];
|
|
UIFontDescriptorSymbolicTraits symbolicTraits = fontDescriptor.symbolicTraits;
|
|
if (isItalic) {
|
|
symbolicTraits |= UIFontDescriptorTraitItalic;
|
|
}
|
|
if (isCondensed) {
|
|
symbolicTraits |= UIFontDescriptorTraitCondensed;
|
|
}
|
|
fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits];
|
|
font = [UIFont fontWithDescriptor:fontDescriptor size:fontSize];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Gracefully handle being given a font name rather than font family, for
|
|
// example: "Helvetica Light Oblique" rather than just "Helvetica".
|
|
if (!didFindFont && fontNamesForFamilyName(familyName).count == 0) {
|
|
font = [UIFont fontWithName:familyName size:fontSize];
|
|
if (font) {
|
|
// It's actually a font name, not a font family name,
|
|
// but we'll do what was meant, not what was said.
|
|
familyName = font.familyName;
|
|
fontWeight = weight ? fontWeight : weightOfFont(font);
|
|
isItalic = style ? isItalic : isItalicFont(font);
|
|
isCondensed = isCondensedFont(font);
|
|
} else {
|
|
// Not a valid font or family
|
|
RCTLogError(@"Unrecognized font family '%@'", familyName);
|
|
if ([UIFont respondsToSelector:@selector(systemFontOfSize:weight:)]) {
|
|
font = [UIFont systemFontOfSize:fontSize weight:fontWeight];
|
|
} else if (fontWeight > UIFontWeightRegular) {
|
|
font = [UIFont boldSystemFontOfSize:fontSize];
|
|
} else {
|
|
font = [UIFont systemFontOfSize:fontSize];
|
|
}
|
|
}
|
|
}
|
|
|
|
NSArray<NSString *> *names = fontNamesForFamilyName(familyName);
|
|
if (!didFindFont) {
|
|
// Get the closest font that matches the given weight for the fontFamily
|
|
CGFloat closestWeight = INFINITY;
|
|
for (NSString *name in names) {
|
|
UIFont *match = [UIFont fontWithName:name size:fontSize];
|
|
if (isItalic == isItalicFont(match) && isCondensed == isCondensedFont(match)) {
|
|
CGFloat testWeight = weightOfFont(match);
|
|
if (ABS(testWeight - fontWeight) < ABS(closestWeight - fontWeight)) {
|
|
font = match;
|
|
closestWeight = testWeight;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
if (!font && names.count > 0) {
|
|
font = [UIFont fontWithName:names[0] size:fontSize];
|
|
}
|
|
|
|
// Apply font variants to font object
|
|
if (variant) {
|
|
NSArray *fontFeatures = [RCTConvert RCTFontVariantDescriptorArray:variant];
|
|
UIFontDescriptor *fontDescriptor = [font.fontDescriptor
|
|
fontDescriptorByAddingAttributes:@{UIFontDescriptorFeatureSettingsAttribute : fontFeatures}];
|
|
font = [UIFont fontWithDescriptor:fontDescriptor size:fontSize];
|
|
}
|
|
|
|
return font;
|
|
}
|
|
|
|
+ (UIFont *)updateFont:(UIFont *)font withFamily:(NSString *)family
|
|
{
|
|
return [self updateFont:font withFamily:family size:nil weight:nil style:nil variant:nil scaleMultiplier:1];
|
|
}
|
|
|
|
+ (UIFont *)updateFont:(UIFont *)font withSize:(NSNumber *)size
|
|
{
|
|
return [self updateFont:font withFamily:nil size:size weight:nil style:nil variant:nil scaleMultiplier:1];
|
|
}
|
|
|
|
+ (UIFont *)updateFont:(UIFont *)font withWeight:(NSString *)weight
|
|
{
|
|
return [self updateFont:font withFamily:nil size:nil weight:weight style:nil variant:nil scaleMultiplier:1];
|
|
}
|
|
|
|
+ (UIFont *)updateFont:(UIFont *)font withStyle:(NSString *)style
|
|
{
|
|
return [self updateFont:font withFamily:nil size:nil weight:nil style:style variant:nil scaleMultiplier:1];
|
|
}
|
|
|
|
@end
|