Skip to content

Commit 8415753

Browse files
evankatz14meta-codesync[bot]
authored andcommitted
Add variable font settings support (#57815)
Summary: Pull Request resolved: #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
1 parent a688608 commit 8415753

8 files changed

Lines changed: 169 additions & 1 deletion

File tree

packages/react-native/React/Tests/Text/RCTAttributedTextUtilsTest.mm

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
#import <CoreText/CoreText.h>
89
#import <Foundation/Foundation.h>
910
#import <XCTest/XCTest.h>
1011

1112
#import <react/renderer/textlayoutmanager/RCTAttributedTextUtils.h>
13+
#import <react/renderer/textlayoutmanager/RCTFontUtils.h>
1214

1315
#include <react/renderer/attributedstring/conversions.h>
1416
#include <react/renderer/core/RawValue.h>
1517

18+
#include <utility>
19+
1620
using namespace facebook::react;
1721

1822
@interface RCTAttributedTextUtilsTest : XCTestCase
@@ -37,6 +41,88 @@ static NSTextAlignment NSTextAlignmentFromTextAlign(NSString *textAlign, LayoutD
3741
return paragraphStyle.alignment;
3842
}
3943

44+
static UIFont *FontFromSettings(std::optional<std::string> settings, std::optional<FontWeight> weight = std::nullopt)
45+
{
46+
TextAttributes textAttributes;
47+
textAttributes.fontVariationSettings = std::move(settings);
48+
textAttributes.fontWeight = weight;
49+
50+
NSDictionary<NSAttributedStringKey, id> *attributes = RCTNSTextAttributesFromTextAttributes(textAttributes);
51+
return attributes[NSFontAttributeName];
52+
}
53+
54+
static NSDictionary<NSNumber *, NSNumber *> *FontVariationsFromSettings(std::optional<std::string> settings)
55+
{
56+
UIFont *font = FontFromSettings(std::move(settings));
57+
return [font.fontDescriptor objectForKey:(UIFontDescriptorAttributeName)kCTFontVariationAttribute];
58+
}
59+
60+
- (void)testFontVariationSettings
61+
{
62+
NSDictionary<NSNumber *, NSNumber *> *variations = FontVariationsFromSettings("'wght' 550, \"opsz\" 18.5");
63+
64+
XCTAssertEqualObjects(variations[@(0x77676874)], @550);
65+
XCTAssertEqualObjects(variations[@(0x6F70737A)], @18.5);
66+
}
67+
68+
- (void)testFontVariationSettingsParser
69+
{
70+
NSDictionary<NSNumber *, NSNumber *> *variations =
71+
RCTParseFontVariationSettings(@"'a,bc' 1, \"wght\" -2.5e1, 'wght' 550");
72+
73+
XCTAssertEqualObjects(variations[@(0x612C6263)], @1);
74+
XCTAssertEqualObjects(variations[@(0x77676874)], @550);
75+
}
76+
77+
- (void)testFontVariationSettingsOverrideFontWeight
78+
{
79+
UIFont *font = FontFromSettings("'wght' 450", FontWeight::Weight700);
80+
NSDictionary<NSNumber *, NSNumber *> *variations =
81+
[font.fontDescriptor objectForKey:(UIFontDescriptorAttributeName)kCTFontVariationAttribute];
82+
83+
XCTAssertEqualObjects(variations[@(0x77676874)], @450);
84+
}
85+
86+
- (void)testInvalidFontVariationSettingsParserValues
87+
{
88+
NSArray<NSString *> *invalidSettings = @[
89+
@"'weight' 550",
90+
@"'wght' 1e999",
91+
@"'wght' NaN",
92+
@"'wght' 550,",
93+
@", 'wght' 550",
94+
@"'wght' 550,, 'opsz' 18",
95+
];
96+
97+
for (NSString *settings in invalidSettings) {
98+
XCTAssertEqual(RCTParseFontVariationSettings(settings).count, 0, @"%@ should be invalid", settings);
99+
}
100+
}
101+
102+
- (void)testInvalidFontVariationSettingsClearVariations
103+
{
104+
NSDictionary<NSNumber *, NSNumber *> *variations = FontVariationsFromSettings("'weight' 550");
105+
NSDictionary<NSNumber *, NSNumber *> *defaultVariations = FontVariationsFromSettings(std::nullopt);
106+
107+
XCTAssertEqualObjects(variations, defaultVariations);
108+
}
109+
110+
- (void)testEmptyFontVariationSettingsClearVariations
111+
{
112+
NSDictionary<NSNumber *, NSNumber *> *variations = FontVariationsFromSettings("");
113+
NSDictionary<NSNumber *, NSNumber *> *defaultVariations = FontVariationsFromSettings(std::nullopt);
114+
115+
XCTAssertEqualObjects(variations, defaultVariations);
116+
}
117+
118+
- (void)testNormalFontVariationSettingsClearVariations
119+
{
120+
NSDictionary<NSNumber *, NSNumber *> *variations = FontVariationsFromSettings("normal");
121+
NSDictionary<NSNumber *, NSNumber *> *defaultVariations = FontVariationsFromSettings(std::nullopt);
122+
123+
XCTAssertEqualObjects(variations, defaultVariations);
124+
}
125+
40126
- (void)testTextAlignmentStartAndEndResolveWithLayoutDirection
41127
{
42128
XCTAssertEqual(NSTextAlignmentFromTextAlign(@"start", LayoutDirection::LeftToRight), NSTextAlignmentLeft);

packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex
154154
fontProperties.weight = textAttributes.fontWeight.has_value()
155155
? RCTUIFontWeightFromInteger((NSInteger)textAttributes.fontWeight.value())
156156
: NAN;
157+
if (textAttributes.fontVariationSettings.has_value()) {
158+
NSString *variationSettings = [NSString stringWithUTF8String:textAttributes.fontVariationSettings->c_str()];
159+
fontProperties.variations = RCTParseFontVariationSettings(variationSettings);
160+
}
157161
fontProperties.sizeMultiplier = RCTEffectiveFontSizeMultiplierFromTextAttributes(textAttributes);
158162

159163
return RCTFontWithFontProperties(fontProperties);

packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontProperties.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ struct RCTFontProperties {
5252
UIFontWeight weight = NAN;
5353
RCTFontStyle style = RCTFontStyleUndefined;
5454
RCTFontVariant variant = RCTFontVariantUndefined;
55+
NSDictionary<NSNumber *, NSNumber *> *_Nullable variations = nil;
5556
CGFloat sizeMultiplier = NAN;
5657
};
5758

packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
#pragma once
9+
810
#import <React/RCTDefines.h>
911
#import <UIKit/UIKit.h>
1012
#import <react/renderer/textlayoutmanager/RCTFontProperties.h>
@@ -19,6 +21,15 @@ using RCTDefaultFontResolver = UIFont *__nullable (^)(const RCTFontProperties &)
1921
*/
2022
RCT_EXTERN void RCTSetDefaultFontResolver(RCTDefaultFontResolver handler);
2123

24+
/**
25+
* Parses comma-separated, quoted four-byte ASCII axis tags and finite numeric
26+
* values into CoreText axis values. `normal`, an empty string, and invalid
27+
* settings normalize to an empty dictionary. Because text inheritance is
28+
* resolved before platform parsing, invalid child settings do not fall back to
29+
* inherited settings.
30+
*/
31+
RCT_EXTERN NSDictionary<NSNumber *, NSNumber *> *RCTParseFontVariationSettings(NSString *variationSettings);
32+
2233
/**
2334
* Returns UIFont instance corresponded to given font properties.
2435
*/

packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#import <CoreText/CoreText.h>
1111
#import <React/RCTFont+Private.h>
1212
#import <React/RCTFont.h>
13+
#import <React/RCTLog.h>
1314

1415
#import <algorithm>
1516
#import <cmath>
@@ -248,6 +249,58 @@ static RCTFontStyle RCTGetFontStyle(UIFont *font)
248249
return fontFeatures;
249250
}
250251

252+
NSDictionary<NSNumber *, NSNumber *> *RCTParseFontVariationSettings(NSString *variationSettings)
253+
{
254+
NSString *trimmedSettings =
255+
[variationSettings stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
256+
if (trimmedSettings.length == 0 || [trimmedSettings isEqualToString:@"normal"]) {
257+
return @{};
258+
}
259+
260+
static NSRegularExpression *variationExpression;
261+
static dispatch_once_t onceToken;
262+
dispatch_once(&onceToken, ^{
263+
variationExpression =
264+
[NSRegularExpression regularExpressionWithPattern:
265+
@R"(\s*(['"])([ -~]{4})\1\s+([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)\s*(,|$))"
266+
options:0
267+
error:nil];
268+
});
269+
270+
NSMutableDictionary<NSNumber *, NSNumber *> *variations = [NSMutableDictionary dictionary];
271+
NSUInteger location = 0;
272+
while (location < variationSettings.length) {
273+
NSTextCheckingResult *match =
274+
[variationExpression firstMatchInString:variationSettings
275+
options:NSMatchingAnchored
276+
range:NSMakeRange(location, variationSettings.length - location)];
277+
if (match == nil || match.range.location != location) {
278+
RCTLogWarn(@"Invalid fontVariationSettings value: %@", variationSettings);
279+
return @{};
280+
}
281+
282+
NSString *axis = [variationSettings substringWithRange:[match rangeAtIndex:2]];
283+
uint32_t axisIdentifier = (uint32_t)[axis characterAtIndex:0] << 24 | (uint32_t)[axis characterAtIndex:1] << 16 |
284+
(uint32_t)[axis characterAtIndex:2] << 8 | (uint32_t)[axis characterAtIndex:3];
285+
NSString *valueString = [variationSettings substringWithRange:[match rangeAtIndex:3]];
286+
double value = valueString.doubleValue;
287+
if (!std::isfinite(value)) {
288+
RCTLogWarn(@"Invalid fontVariationSettings value: %@", variationSettings);
289+
return @{};
290+
}
291+
variations[@(axisIdentifier)] = @(value);
292+
293+
NSString *separator = [variationSettings substringWithRange:[match rangeAtIndex:4]];
294+
location = NSMaxRange(match.range);
295+
if ([separator isEqualToString:@","] && location == variationSettings.length) {
296+
RCTLogWarn(@"Invalid fontVariationSettings value: %@", variationSettings);
297+
return @{};
298+
}
299+
}
300+
301+
return variations;
302+
}
303+
251304
static RCTDefaultFontResolver defaultFontResolver;
252305

253306
void RCTSetDefaultFontResolver(RCTDefaultFontResolver handler)
@@ -278,7 +331,9 @@ void RCTSetDefaultFontResolver(RCTDefaultFontResolver handler)
278331

279332
if (font == nil) {
280333
if (defaultFontResolver != nil) {
281-
font = defaultFontResolver(fontProperties);
334+
RCTFontProperties baseFontProperties = fontProperties;
335+
baseFontProperties.variations = nil;
336+
font = defaultFontResolver(baseFontProperties);
282337
}
283338

284339
if (font == nil) {
@@ -402,5 +457,13 @@ static UIFontDescriptorSystemDesign RCTGetFontDescriptorSystemDesign(NSString *f
402457
font = [UIFont fontWithDescriptor:fontDescriptor size:effectiveFontSize];
403458
}
404459

460+
NSDictionary<NSNumber *, NSNumber *> *variations = fontProperties.variations;
461+
if (variations != nil) {
462+
UIFontDescriptor *fontDescriptor = [font.fontDescriptor fontDescriptorByAddingAttributes:@{
463+
(UIFontDescriptorAttributeName)kCTFontVariationAttribute : variations,
464+
}];
465+
font = [UIFont fontWithDescriptor:fontDescriptor size:effectiveFontSize];
466+
}
467+
405468
return font;
406469
}

scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3186,6 +3186,7 @@ struct RCTCornerRadii {
31863186
struct RCTFontProperties {
31873187
public CGFloat size;
31883188
public CGFloat sizeMultiplier;
3189+
public NSDictionary<NSNumber*, NSNumber*>* _Nullable variations;
31893190
public NSString* family;
31903191
public RCTFontStyle style;
31913192
public RCTFontVariant variant;

scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3174,6 +3174,7 @@ struct RCTCornerRadii {
31743174
struct RCTFontProperties {
31753175
public CGFloat size;
31763176
public CGFloat sizeMultiplier;
3177+
public NSDictionary<NSNumber*, NSNumber*>* _Nullable variations;
31773178
public NSString* family;
31783179
public RCTFontStyle style;
31793180
public RCTFontVariant variant;

scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3186,6 +3186,7 @@ struct RCTCornerRadii {
31863186
struct RCTFontProperties {
31873187
public CGFloat size;
31883188
public CGFloat sizeMultiplier;
3189+
public NSDictionary<NSNumber*, NSNumber*>* _Nullable variations;
31893190
public NSString* family;
31903191
public RCTFontStyle style;
31913192
public RCTFontVariant variant;

0 commit comments

Comments
 (0)