Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,7 @@ export type AndroidTextInputNativeProps = Readonly<{
includeFontPadding?: ?boolean,
fontWeight?: ?string,
fontFamily?: ?string,
fontVariationSettings?: ?string,

/**
* I cannot find where these are defined but JS complains without them.
Expand Down Expand Up @@ -714,6 +715,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
includeFontPadding: true,
fontWeight: true,
fontFamily: true,
fontVariationSettings: true,
allowFontScaling: true,
onSelectionChange: true,
mostRecentEventCount: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
fontSize: true,
fontStyle: true,
fontVariant: fontVariantAttribute,
fontVariationSettings: true,
fontWeight: true,
includeFontPadding: true,
letterSpacing: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,12 @@ export interface TextStyle extends TextStyleIOS, TextStyleAndroid, ViewStyle {
| 'heavy'
| 'black'
| undefined;
/**
* Specifies OpenType font variation axis values using CSS syntax. An empty
* string resets inherited variation settings. On Android, this requires API
* level 26 or later.
*/
fontVariationSettings?: string | undefined;
letterSpacing?: number | undefined;
lineHeight?: number | undefined;
textAlign?:
Expand Down
6 changes: 6 additions & 0 deletions packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,12 @@ type ____TextStyle_InternalBase = Readonly<{
*/
fontWeight?: ____FontWeight_Internal,
fontVariant?: ____FontVariantArray_Internal | string,
/**
* Specifies OpenType font variation axis values using CSS syntax. An empty
* string resets inherited variation settings. On Android, this requires API
* level 26 or later.
*/
fontVariationSettings?: string,
textShadowOffset?: Readonly<{
width: number,
height: number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -4543,6 +4543,7 @@ public final class com/facebook/react/uimanager/ViewProps {
public static final field FONT_SIZE Ljava/lang/String;
public static final field FONT_STYLE Ljava/lang/String;
public static final field FONT_VARIANT Ljava/lang/String;
public static final field FONT_VARIATION_SETTINGS Ljava/lang/String;
public static final field FONT_WEIGHT Ljava/lang/String;
public static final field FOREGROUND_COLOR Ljava/lang/String;
public static final field GAP Ljava/lang/String;
Expand Down Expand Up @@ -6140,6 +6141,7 @@ public final class com/facebook/react/views/text/TextAttributeProps {
public static final field TA_KEY_FONT_SIZE_MULTIPLIER I
public static final field TA_KEY_FONT_STYLE I
public static final field TA_KEY_FONT_VARIANT I
public static final field TA_KEY_FONT_VARIATION_SETTINGS I
public static final field TA_KEY_FONT_WEIGHT I
public static final field TA_KEY_FOREGROUND_COLOR I
public static final field TA_KEY_IS_HIGHLIGHTED I
Expand Down Expand Up @@ -6170,6 +6172,7 @@ public final class com/facebook/react/views/text/TextAttributeProps {
public final fun getFontFeatureSettings ()Ljava/lang/String;
public final fun getFontSize ()I
public final fun getFontStyle ()I
public final fun getFontVariationSettings ()Ljava/lang/String;
public final fun getFontWeight ()I
public final fun getLayoutDirection ()I
public final fun getLetterSpacing ()F
Expand Down Expand Up @@ -6358,6 +6361,7 @@ public class com/facebook/react/views/textinput/ReactTextInputManager : com/face
public final fun setFontSize (Lcom/facebook/react/views/textinput/ReactEditText;F)V
public final fun setFontStyle (Lcom/facebook/react/views/textinput/ReactEditText;Ljava/lang/String;)V
public final fun setFontVariant (Lcom/facebook/react/views/textinput/ReactEditText;Lcom/facebook/react/bridge/ReadableArray;)V
public final fun setFontVariationSettings (Lcom/facebook/react/views/textinput/ReactEditText;Ljava/lang/String;)V
public final fun setFontWeight (Lcom/facebook/react/views/textinput/ReactEditText;Ljava/lang/String;)V
public final fun setImportantForAutofill (Lcom/facebook/react/views/textinput/ReactEditText;Ljava/lang/String;)V
public final fun setIncludeFontPadding (Lcom/facebook/react/views/textinput/ReactEditText;Z)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public object ViewProps {
public const val FONT_WEIGHT: String = "fontWeight"
public const val FONT_STYLE: String = "fontStyle"
public const val FONT_VARIANT: String = "fontVariant"
public const val FONT_VARIATION_SETTINGS: String = "fontVariationSettings"
public const val FONT_FAMILY: String = "fontFamily"
public const val LINE_HEIGHT: String = "lineHeight"
public const val LETTER_SPACING: String = "letterSpacing"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ package com.facebook.react.views.text
import android.content.Context
import android.content.res.AssetManager
import android.content.res.Configuration
import android.graphics.Paint
import android.graphics.Typeface
import android.graphics.fonts.FontVariationAxis
import android.os.Build
import com.facebook.common.logging.FLog
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.common.ReactConstants
import com.facebook.react.common.assets.ReactFontManager
Expand Down Expand Up @@ -151,4 +154,44 @@ public object ReactTypefaceUtils {

return Typeface.create(baseTypeface, adjustedWeight, italic)
}

internal fun parseFontVariationSettings(fontVariationSettings: String?): String? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return null
}

if (fontVariationSettings.isNullOrEmpty()) {
return fontVariationSettings
}

if (fontVariationSettings.trim().equals("normal", ignoreCase = true)) {
return ""
}

return try {
FontVariationAxis.fromFontVariationSettings(fontVariationSettings)
fontVariationSettings
} catch (exception: IllegalArgumentException) {
FLog.w(ReactConstants.TAG, "Invalid fontVariationSettings: $fontVariationSettings")
null
}
}

internal fun applyFontVariationSettings(paint: Paint, fontVariationSettings: String?) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
}

try {
paint.setFontVariationSettings(fontVariationSettings)
} catch (exception: IllegalArgumentException) {
// Paint instances are reused, so explicitly clear axes from a previous layout.
paint.setFontVariationSettings(null)
FLog.w(
ReactConstants.TAG,
"Invalid fontVariationSettings: $fontVariationSettings",
exception,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.facebook.react.uimanager.ReactStylesDiffMap
import com.facebook.react.uimanager.ViewProps
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontVariant
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontVariationSettings
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
import kotlin.math.ceil

Expand Down Expand Up @@ -153,6 +154,10 @@ public class TextAttributeProps private constructor() {
public var fontFeatureSettings: String? = null
private set

/** @see android.graphics.Paint.setFontVariationSettings */
public var fontVariationSettings: String? = null
private set

@Deprecated("Use lineHeight instead", ReplaceWith("lineHeight"))
public val effectiveLineHeight: Float
get() = lineHeight
Expand Down Expand Up @@ -396,6 +401,7 @@ public class TextAttributeProps private constructor() {
public const val TA_KEY_TEXT_TRANSFORM: Int = 27
public const val TA_KEY_MAX_FONT_SIZE_MULTIPLIER: Int = 29
public const val TA_KEY_TEXT_EFFECTS: Int = 30
public const val TA_KEY_FONT_VARIATION_SETTINGS: Int = 31
private const val TE_KEY_NAME: Int = 0
private const val TE_KEY_PROPS: Int = 1

Expand Down Expand Up @@ -433,6 +439,8 @@ public class TextAttributeProps private constructor() {
TA_KEY_FONT_WEIGHT -> result.setFontWeight(entry.stringValue)
TA_KEY_FONT_STYLE -> result.setFontStyle(entry.stringValue)
TA_KEY_FONT_VARIANT -> result.setFontVariant(entry.mapBufferValue)
TA_KEY_FONT_VARIATION_SETTINGS ->
result.fontVariationSettings = parseFontVariationSettings(entry.stringValue)
TA_KEY_ALLOW_FONT_SCALING -> result.allowFontScaling = entry.booleanValue
TA_KEY_LETTER_SPACING -> result.letterSpacing = entry.doubleValue.toFloat()
TA_KEY_LINE_HEIGHT -> result.lineHeight = entry.doubleValue.toFloat()
Expand Down Expand Up @@ -500,6 +508,8 @@ public class TextAttributeProps private constructor() {
result.setFontWeight(getStringProp(props, ViewProps.FONT_WEIGHT))
result.setFontStyle(getStringProp(props, ViewProps.FONT_STYLE))
result.setFontVariant(getArrayProp(props, ViewProps.FONT_VARIANT))
result.fontVariationSettings =
parseFontVariationSettings(getStringProp(props, ViewProps.FONT_VARIATION_SETTINGS))
result.includeFontPadding = getBooleanProp(props, ViewProps.INCLUDE_FONT_PADDING, true)
result.setTextDecorationLine(getStringProp(props, ViewProps.TEXT_DECORATION_LINE))
result.textDecorationColor =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ internal object TextLayoutManager {
if (
textAttributes.fontStyle != ReactConstants.UNSET ||
textAttributes.fontWeight != ReactConstants.UNSET ||
textAttributes.fontVariationSettings != null ||
textAttributes.fontFamily != null
) {
ops.add(
Expand All @@ -323,6 +324,7 @@ internal object TextLayoutManager {
textAttributes.fontStyle,
textAttributes.fontWeight,
textAttributes.fontFeatureSettings,
textAttributes.fontVariationSettings,
textAttributes.fontFamily,
assets,
fontWeightAdjustment,
Expand Down Expand Up @@ -547,13 +549,15 @@ internal object TextLayoutManager {
if (
fragment.props.fontStyle != ReactConstants.UNSET ||
fragment.props.fontWeight != ReactConstants.UNSET ||
fragment.props.fontVariationSettings != null ||
fragment.props.fontFamily != null
) {
spannable.setSpan(
CustomStyleSpan(
fragment.props.fontStyle,
fragment.props.fontWeight,
fragment.props.fontFeatureSettings,
fragment.props.fontVariationSettings,
fragment.props.fontFamily,
assets,
fontWeightAdjustment,
Expand Down Expand Up @@ -883,6 +887,7 @@ internal object TextLayoutManager {
if (
baseTextAttributes.fontStyle != ReactConstants.UNSET ||
baseTextAttributes.fontWeight != ReactConstants.UNSET ||
baseTextAttributes.fontVariationSettings != null ||
baseTextAttributes.fontFamily != null
) {
val typeface =
Expand Down Expand Up @@ -912,6 +917,8 @@ internal object TextLayoutManager {
paint.setTypeface(typeface)
}
}

ReactTypefaceUtils.applyFontVariationSettings(paint, baseTextAttributes.fontVariationSettings)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal class CustomStyleSpan(
private val privateStyle: Int,
private val privateWeight: Int,
val fontFeatureSettings: String?,
val fontVariationSettings: String?,
val fontFamily: String?,
private val assetManager: AssetManager,
private val fontWeightAdjustment: Int = 0,
Expand All @@ -41,6 +42,7 @@ internal class CustomStyleSpan(
privateStyle,
privateWeight,
fontFeatureSettings,
fontVariationSettings,
fontFamily,
assetManager,
fontWeightAdjustment,
Expand All @@ -53,6 +55,7 @@ internal class CustomStyleSpan(
privateStyle,
privateWeight,
fontFeatureSettings,
fontVariationSettings,
fontFamily,
assetManager,
fontWeightAdjustment,
Expand Down Expand Up @@ -81,6 +84,7 @@ internal class CustomStyleSpan(
style: Int,
weight: Int,
fontFeatureSettingsParam: String?,
fontVariationSettingsParam: String?,
family: String?,
assetManager: AssetManager,
fontWeightAdjustment: Int,
Expand All @@ -92,6 +96,7 @@ internal class CustomStyleSpan(
paint.apply {
fontFeatureSettings = fontFeatureSettingsParam
setTypeface(adjustedTypeface)
ReactTypefaceUtils.applyFontVariationSettings(this, fontVariationSettingsParam)
isSubpixelText = true
isLinearText = true
}
Expand Down
Loading