mirror of
https://github.com/react/react-native.git
synced 2026-09-28 05:13:06 +08:00
Add variable font settings support (#57815)
Summary: Pull Request resolved: https://github.com/react/react-native/pull/57815 Apply the existing `fontVariationSettings` text style prop when Fabric constructs fonts on iOS. Parse CSS-compatible axis settings into CoreText variation dictionaries while preserving absent, explicit-clear, and invalid value semantics for nested text. The parser supports quoted four-character OpenType tags and finite numeric values, rejects malformed settings as a complete unit, and applies normalized variations after the base font and feature settings are resolved. This shared attributed-text path covers Fabric `Text` and `TextInput`. Changelog: [iOS][Added] - Add `fontVariationSettings` support for Fabric text Reviewed By: Abbondanzo, christophpurrer Differential Revision: D114121940 fbshipit-source-id: d2b2fffd4fe723c5205e5279a466a125aa7edd38
This commit is contained in:
committed by
meta-codesync[bot]
parent
a688608090
commit
8415753e21
@@ -5,14 +5,18 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#import <CoreText/CoreText.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import <react/renderer/textlayoutmanager/RCTAttributedTextUtils.h>
|
||||
#import <react/renderer/textlayoutmanager/RCTFontUtils.h>
|
||||
|
||||
#include <react/renderer/attributedstring/conversions.h>
|
||||
#include <react/renderer/core/RawValue.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
using namespace facebook::react;
|
||||
|
||||
@interface RCTAttributedTextUtilsTest : XCTestCase
|
||||
@@ -37,6 +41,88 @@ static NSTextAlignment NSTextAlignmentFromTextAlign(NSString *textAlign, LayoutD
|
||||
return paragraphStyle.alignment;
|
||||
}
|
||||
|
||||
static UIFont *FontFromSettings(std::optional<std::string> settings, std::optional<FontWeight> weight = std::nullopt)
|
||||
{
|
||||
TextAttributes textAttributes;
|
||||
textAttributes.fontVariationSettings = std::move(settings);
|
||||
textAttributes.fontWeight = weight;
|
||||
|
||||
NSDictionary<NSAttributedStringKey, id> *attributes = RCTNSTextAttributesFromTextAttributes(textAttributes);
|
||||
return attributes[NSFontAttributeName];
|
||||
}
|
||||
|
||||
static NSDictionary<NSNumber *, NSNumber *> *FontVariationsFromSettings(std::optional<std::string> settings)
|
||||
{
|
||||
UIFont *font = FontFromSettings(std::move(settings));
|
||||
return [font.fontDescriptor objectForKey:(UIFontDescriptorAttributeName)kCTFontVariationAttribute];
|
||||
}
|
||||
|
||||
- (void)testFontVariationSettings
|
||||
{
|
||||
NSDictionary<NSNumber *, NSNumber *> *variations = FontVariationsFromSettings("'wght' 550, \"opsz\" 18.5");
|
||||
|
||||
XCTAssertEqualObjects(variations[@(0x77676874)], @550);
|
||||
XCTAssertEqualObjects(variations[@(0x6F70737A)], @18.5);
|
||||
}
|
||||
|
||||
- (void)testFontVariationSettingsParser
|
||||
{
|
||||
NSDictionary<NSNumber *, NSNumber *> *variations =
|
||||
RCTParseFontVariationSettings(@"'a,bc' 1, \"wght\" -2.5e1, 'wght' 550");
|
||||
|
||||
XCTAssertEqualObjects(variations[@(0x612C6263)], @1);
|
||||
XCTAssertEqualObjects(variations[@(0x77676874)], @550);
|
||||
}
|
||||
|
||||
- (void)testFontVariationSettingsOverrideFontWeight
|
||||
{
|
||||
UIFont *font = FontFromSettings("'wght' 450", FontWeight::Weight700);
|
||||
NSDictionary<NSNumber *, NSNumber *> *variations =
|
||||
[font.fontDescriptor objectForKey:(UIFontDescriptorAttributeName)kCTFontVariationAttribute];
|
||||
|
||||
XCTAssertEqualObjects(variations[@(0x77676874)], @450);
|
||||
}
|
||||
|
||||
- (void)testInvalidFontVariationSettingsParserValues
|
||||
{
|
||||
NSArray<NSString *> *invalidSettings = @[
|
||||
@"'weight' 550",
|
||||
@"'wght' 1e999",
|
||||
@"'wght' NaN",
|
||||
@"'wght' 550,",
|
||||
@", 'wght' 550",
|
||||
@"'wght' 550,, 'opsz' 18",
|
||||
];
|
||||
|
||||
for (NSString *settings in invalidSettings) {
|
||||
XCTAssertEqual(RCTParseFontVariationSettings(settings).count, 0, @"%@ should be invalid", settings);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testInvalidFontVariationSettingsClearVariations
|
||||
{
|
||||
NSDictionary<NSNumber *, NSNumber *> *variations = FontVariationsFromSettings("'weight' 550");
|
||||
NSDictionary<NSNumber *, NSNumber *> *defaultVariations = FontVariationsFromSettings(std::nullopt);
|
||||
|
||||
XCTAssertEqualObjects(variations, defaultVariations);
|
||||
}
|
||||
|
||||
- (void)testEmptyFontVariationSettingsClearVariations
|
||||
{
|
||||
NSDictionary<NSNumber *, NSNumber *> *variations = FontVariationsFromSettings("");
|
||||
NSDictionary<NSNumber *, NSNumber *> *defaultVariations = FontVariationsFromSettings(std::nullopt);
|
||||
|
||||
XCTAssertEqualObjects(variations, defaultVariations);
|
||||
}
|
||||
|
||||
- (void)testNormalFontVariationSettingsClearVariations
|
||||
{
|
||||
NSDictionary<NSNumber *, NSNumber *> *variations = FontVariationsFromSettings("normal");
|
||||
NSDictionary<NSNumber *, NSNumber *> *defaultVariations = FontVariationsFromSettings(std::nullopt);
|
||||
|
||||
XCTAssertEqualObjects(variations, defaultVariations);
|
||||
}
|
||||
|
||||
- (void)testTextAlignmentStartAndEndResolveWithLayoutDirection
|
||||
{
|
||||
XCTAssertEqual(NSTextAlignmentFromTextAlign(@"start", LayoutDirection::LeftToRight), NSTextAlignmentLeft);
|
||||
|
||||
+4
@@ -154,6 +154,10 @@ inline static UIFont *RCTEffectiveFontFromTextAttributes(const TextAttributes &t
|
||||
fontProperties.weight = textAttributes.fontWeight.has_value()
|
||||
? RCTUIFontWeightFromInteger((NSInteger)textAttributes.fontWeight.value())
|
||||
: NAN;
|
||||
if (textAttributes.fontVariationSettings.has_value()) {
|
||||
NSString *variationSettings = [NSString stringWithUTF8String:textAttributes.fontVariationSettings->c_str()];
|
||||
fontProperties.variations = RCTParseFontVariationSettings(variationSettings);
|
||||
}
|
||||
fontProperties.sizeMultiplier = RCTEffectiveFontSizeMultiplierFromTextAttributes(textAttributes);
|
||||
|
||||
return RCTFontWithFontProperties(fontProperties);
|
||||
|
||||
+1
@@ -52,6 +52,7 @@ struct RCTFontProperties {
|
||||
UIFontWeight weight = NAN;
|
||||
RCTFontStyle style = RCTFontStyleUndefined;
|
||||
RCTFontVariant variant = RCTFontVariantUndefined;
|
||||
NSDictionary<NSNumber *, NSNumber *> *_Nullable variations = nil;
|
||||
CGFloat sizeMultiplier = NAN;
|
||||
};
|
||||
|
||||
|
||||
+11
@@ -5,6 +5,8 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <React/RCTDefines.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <react/renderer/textlayoutmanager/RCTFontProperties.h>
|
||||
@@ -19,6 +21,15 @@ using RCTDefaultFontResolver = UIFont *__nullable (^)(const RCTFontProperties &)
|
||||
*/
|
||||
RCT_EXTERN void RCTSetDefaultFontResolver(RCTDefaultFontResolver handler);
|
||||
|
||||
/**
|
||||
* Parses comma-separated, quoted four-byte ASCII axis tags and finite numeric
|
||||
* values into CoreText axis values. `normal`, an empty string, and invalid
|
||||
* settings normalize to an empty dictionary. Because text inheritance is
|
||||
* resolved before platform parsing, invalid child settings do not fall back to
|
||||
* inherited settings.
|
||||
*/
|
||||
RCT_EXTERN NSDictionary<NSNumber *, NSNumber *> *RCTParseFontVariationSettings(NSString *variationSettings);
|
||||
|
||||
/**
|
||||
* Returns UIFont instance corresponded to given font properties.
|
||||
*/
|
||||
|
||||
+64
-1
@@ -10,6 +10,7 @@
|
||||
#import <CoreText/CoreText.h>
|
||||
#import <React/RCTFont+Private.h>
|
||||
#import <React/RCTFont.h>
|
||||
#import <React/RCTLog.h>
|
||||
|
||||
#import <algorithm>
|
||||
#import <cmath>
|
||||
@@ -248,6 +249,58 @@ static NSArray *RCTFontFeatures(RCTFontVariant fontVariant)
|
||||
return fontFeatures;
|
||||
}
|
||||
|
||||
NSDictionary<NSNumber *, NSNumber *> *RCTParseFontVariationSettings(NSString *variationSettings)
|
||||
{
|
||||
NSString *trimmedSettings =
|
||||
[variationSettings stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
if (trimmedSettings.length == 0 || [trimmedSettings isEqualToString:@"normal"]) {
|
||||
return @{};
|
||||
}
|
||||
|
||||
static NSRegularExpression *variationExpression;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
variationExpression =
|
||||
[NSRegularExpression regularExpressionWithPattern:
|
||||
@R"(\s*(['"])([ -~]{4})\1\s+([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)\s*(,|$))"
|
||||
options:0
|
||||
error:nil];
|
||||
});
|
||||
|
||||
NSMutableDictionary<NSNumber *, NSNumber *> *variations = [NSMutableDictionary dictionary];
|
||||
NSUInteger location = 0;
|
||||
while (location < variationSettings.length) {
|
||||
NSTextCheckingResult *match =
|
||||
[variationExpression firstMatchInString:variationSettings
|
||||
options:NSMatchingAnchored
|
||||
range:NSMakeRange(location, variationSettings.length - location)];
|
||||
if (match == nil || match.range.location != location) {
|
||||
RCTLogWarn(@"Invalid fontVariationSettings value: %@", variationSettings);
|
||||
return @{};
|
||||
}
|
||||
|
||||
NSString *axis = [variationSettings substringWithRange:[match rangeAtIndex:2]];
|
||||
uint32_t axisIdentifier = (uint32_t)[axis characterAtIndex:0] << 24 | (uint32_t)[axis characterAtIndex:1] << 16 |
|
||||
(uint32_t)[axis characterAtIndex:2] << 8 | (uint32_t)[axis characterAtIndex:3];
|
||||
NSString *valueString = [variationSettings substringWithRange:[match rangeAtIndex:3]];
|
||||
double value = valueString.doubleValue;
|
||||
if (!std::isfinite(value)) {
|
||||
RCTLogWarn(@"Invalid fontVariationSettings value: %@", variationSettings);
|
||||
return @{};
|
||||
}
|
||||
variations[@(axisIdentifier)] = @(value);
|
||||
|
||||
NSString *separator = [variationSettings substringWithRange:[match rangeAtIndex:4]];
|
||||
location = NSMaxRange(match.range);
|
||||
if ([separator isEqualToString:@","] && location == variationSettings.length) {
|
||||
RCTLogWarn(@"Invalid fontVariationSettings value: %@", variationSettings);
|
||||
return @{};
|
||||
}
|
||||
}
|
||||
|
||||
return variations;
|
||||
}
|
||||
|
||||
static RCTDefaultFontResolver defaultFontResolver;
|
||||
|
||||
void RCTSetDefaultFontResolver(RCTDefaultFontResolver handler)
|
||||
@@ -278,7 +331,9 @@ static UIFont *RCTDefaultFontWithFontProperties(const RCTFontProperties &fontPro
|
||||
|
||||
if (font == nil) {
|
||||
if (defaultFontResolver != nil) {
|
||||
font = defaultFontResolver(fontProperties);
|
||||
RCTFontProperties baseFontProperties = fontProperties;
|
||||
baseFontProperties.variations = nil;
|
||||
font = defaultFontResolver(baseFontProperties);
|
||||
}
|
||||
|
||||
if (font == nil) {
|
||||
@@ -402,5 +457,13 @@ UIFont *RCTFontWithFontProperties(RCTFontProperties fontProperties)
|
||||
font = [UIFont fontWithDescriptor:fontDescriptor size:effectiveFontSize];
|
||||
}
|
||||
|
||||
NSDictionary<NSNumber *, NSNumber *> *variations = fontProperties.variations;
|
||||
if (variations != nil) {
|
||||
UIFontDescriptor *fontDescriptor = [font.fontDescriptor fontDescriptorByAddingAttributes:@{
|
||||
(UIFontDescriptorAttributeName)kCTFontVariationAttribute : variations,
|
||||
}];
|
||||
font = [UIFont fontWithDescriptor:fontDescriptor size:effectiveFontSize];
|
||||
}
|
||||
|
||||
return font;
|
||||
}
|
||||
|
||||
@@ -3186,6 +3186,7 @@ struct RCTCornerRadii {
|
||||
struct RCTFontProperties {
|
||||
public CGFloat size;
|
||||
public CGFloat sizeMultiplier;
|
||||
public NSDictionary<NSNumber*, NSNumber*>* _Nullable variations;
|
||||
public NSString* family;
|
||||
public RCTFontStyle style;
|
||||
public RCTFontVariant variant;
|
||||
|
||||
@@ -3174,6 +3174,7 @@ struct RCTCornerRadii {
|
||||
struct RCTFontProperties {
|
||||
public CGFloat size;
|
||||
public CGFloat sizeMultiplier;
|
||||
public NSDictionary<NSNumber*, NSNumber*>* _Nullable variations;
|
||||
public NSString* family;
|
||||
public RCTFontStyle style;
|
||||
public RCTFontVariant variant;
|
||||
|
||||
@@ -3186,6 +3186,7 @@ struct RCTCornerRadii {
|
||||
struct RCTFontProperties {
|
||||
public CGFloat size;
|
||||
public CGFloat sizeMultiplier;
|
||||
public NSDictionary<NSNumber*, NSNumber*>* _Nullable variations;
|
||||
public NSString* family;
|
||||
public RCTFontStyle style;
|
||||
public RCTFontVariant variant;
|
||||
|
||||
Reference in New Issue
Block a user