diff --git a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index db39c72950e..3b8bf2cc68e 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -268,6 +268,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { fontFamily: true, fontSize: true, fontStyle: true, + fontFeatureSettings: true, fontVariant: fontVariantAttribute, fontVariationSettings: fontVariationSettingsAttribute, fontWeight: true, diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js index 5a1951a1e2c..f0d707532ef 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js @@ -1009,7 +1009,16 @@ type ____TextStyle_InternalBase = Readonly<{ color?: ____ColorValue_Internal, fontFamily?: string, fontSize?: number, - fontStyle?: 'normal' | 'italic', + /** + * `oblique` slants upright glyphs; `oblique ` gives the slant an explicit CSS angle, + * for example `'oblique 20deg'`. An omitted angle is 14 degrees, the same slant this library + * has always used for synthetic italic. An explicit `'slnt'` in `fontVariationSettings` takes + * precedence over both. + * + * The `string` arm exists only to admit the `oblique ` form, which neither Flow nor the + * generated types can spell more precisely; prefer one of the listed keywords. + */ + fontStyle?: 'normal' | 'italic' | 'oblique' | string, /** * Specifies font weight. The values 'normal' and 'bold' are supported * for most fonts. Not all fonts have a variant for each of the numeric @@ -1017,6 +1026,12 @@ type ____TextStyle_InternalBase = Readonly<{ */ fontWeight?: ____FontWeight_Internal, fontVariant?: ____FontVariantArray_Internal | string, + /** + * Specifies OpenType feature tags using CSS syntax. Combines with + * `fontVariant`; where both name the same tag, `fontFeatureSettings` wins. + * `normal` or an empty string contributes no features of its own. + */ + fontFeatureSettings?: string, /** * Specifies OpenType font variation axis values using CSS syntax or an * object keyed by four-character axis tags. An empty string or object resets diff --git a/packages/react-native/Libraries/Text/__tests__/Text-itest.js b/packages/react-native/Libraries/Text/__tests__/Text-itest.js index 2d19756ff93..f8323fbbf52 100644 --- a/packages/react-native/Libraries/Text/__tests__/Text-itest.js +++ b/packages/react-native/Libraries/Text/__tests__/Text-itest.js @@ -889,6 +889,43 @@ describe('', () => { .toJSONObject().props.fontVariant; expect(fontVariant).toContain('small-caps'); }); + it('propagates fontFeatureSettings to the mounting layer', () => { + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + + {TEST_TEXT} + , + ); + }); + expect( + root.getRenderedOutput({props: ['fontFeatureSettings']}).toJSX(), + ).toEqual( + + {TEST_TEXT} + , + ); + }); + + it('keeps fontFeatureSettings and fontVariant as separate props', () => { + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + + {TEST_TEXT} + , + ); + }); + const props = root + .getRenderedOutput({props: ['fontVariant', 'fontFeatureSettings']}) + .toJSONObject().props; + expect(props.fontVariant).toContain('small-caps'); + expect(props.fontFeatureSettings).toBe("'ss01'"); + }); }); component TestComponent(testID?: ?string, ...props: AccessibilityProps) { diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index f4386d1bd1c..a2f14297ad9 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -4560,6 +4560,7 @@ public final class com/facebook/react/uimanager/ViewProps { public static final field FLEX_SHRINK Ljava/lang/String; public static final field FLEX_WRAP Ljava/lang/String; public static final field FONT_FAMILY Ljava/lang/String; + public static final field FONT_FEATURE_SETTINGS Ljava/lang/String; 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; @@ -6157,6 +6158,7 @@ public final class com/facebook/react/views/text/TextAttributeProps { public static final field TA_KEY_BACKGROUND_COLOR I public static final field TA_KEY_BEST_WRITING_DIRECTION I public static final field TA_KEY_FONT_FAMILY I + public static final field TA_KEY_FONT_FEATURE_SETTINGS I public static final field TA_KEY_FONT_SIZE I public static final field TA_KEY_FONT_SIZE_MULTIPLIER I public static final field TA_KEY_FONT_STYLE I diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt index 8aeb0684837..119a6f6a407 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt @@ -90,6 +90,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_FEATURE_SETTINGS: String = "fontFeatureSettings" public const val FONT_VARIATION_SETTINGS: String = "fontVariationSettings" public const val FONT_FAMILY: String = "fontFamily" public const val LINE_HEIGHT: String = "lineHeight" diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.kt index 154062b966d..3fe2a2b9533 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTypefaceUtils.kt @@ -106,6 +106,31 @@ public object ReactTypefaceUtils { return features.joinToString(", ") } + /** + * Normalizes a CSS `font-feature-settings` value. Per CSS, `normal` means "no author-specified + * features" rather than "clear everything", so it collapses to the empty string and leaves any + * `fontVariant` features intact. Anything else is forwarded unchanged — the shaper ignores tags + * it cannot parse, so narrowing the grammar here would only reject values the platform accepts. + */ + internal fun parseFontFeatureSettings(fontFeatureSettings: String?): String? { + val trimmed = fontFeatureSettings?.trim() ?: return null + return if (trimmed.equals("normal", ignoreCase = true)) "" else trimmed + } + + /** + * Combines the feature contributions of `fontVariant`, the high-level longhands and the author's + * raw `fontFeatureSettings`, which must be passed least- to most-specific so that a tag named + * more than once resolves last-wins. + * + * Mirrors `resolveFontFeatureSettings` in the shared C++ so the legacy [ReactStylesDiffMap] path, + * which never passes through C++, resolves identically to the Fabric path. + */ + internal fun composeFontFeatureSettings(vararg contributions: String?): String? = + // Set but contributing nothing, such as a lone `normal`, is not the same as asking for an + // empty feature list: the platform default has to survive, and a run that names no feature + // must not pick up a span it has nothing to carry. + contributions.filterNot { it.isNullOrEmpty() }.joinToString(", ").ifEmpty { null } + @JvmStatic public fun applyStyles( typeface: Typeface?, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt index 4ebaa5630c0..e97c74cbfa0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt @@ -23,6 +23,8 @@ import com.facebook.react.uimanager.ReactAccessibilityDelegate import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole import com.facebook.react.uimanager.ReactStylesDiffMap import com.facebook.react.uimanager.ViewProps +import com.facebook.react.views.text.ReactTypefaceUtils.composeFontFeatureSettings +import com.facebook.react.views.text.ReactTypefaceUtils.parseFontFeatureSettings import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle import com.facebook.react.views.text.ReactTypefaceUtils.parseFontVariant import com.facebook.react.views.text.ReactTypefaceUtils.parseFontVariationSettings @@ -216,55 +218,6 @@ public class TextAttributeProps private constructor() { // } } - private fun setFontVariant(fontVariant: ReadableArray?) { - fontFeatureSettings = parseFontVariant(fontVariant) - } - - private fun setFontVariant(fontVariant: MapBuffer?) { - if (fontVariant == null || fontVariant.count == 0) { - fontFeatureSettings = null - return - } - - val features: MutableList = ArrayList() - val iterator = fontVariant.iterator() - while (iterator.hasNext()) { - val entry = iterator.next() - val value = entry.stringValue - @Suppress("SENSELESS_COMPARISON") - if (value != null) { - when (value) { - "small-caps" -> features.add("'smcp'") - "oldstyle-nums" -> features.add("'onum'") - "lining-nums" -> features.add("'lnum'") - "tabular-nums" -> features.add("'tnum'") - "proportional-nums" -> features.add("'pnum'") - "stylistic-one" -> features.add("'ss01'") - "stylistic-two" -> features.add("'ss02'") - "stylistic-three" -> features.add("'ss03'") - "stylistic-four" -> features.add("'ss04'") - "stylistic-five" -> features.add("'ss05'") - "stylistic-six" -> features.add("'ss06'") - "stylistic-seven" -> features.add("'ss07'") - "stylistic-eight" -> features.add("'ss08'") - "stylistic-nine" -> features.add("'ss09'") - "stylistic-ten" -> features.add("'ss10'") - "stylistic-eleven" -> features.add("'ss11'") - "stylistic-twelve" -> features.add("'ss12'") - "stylistic-thirteen" -> features.add("'ss13'") - "stylistic-fourteen" -> features.add("'ss14'") - "stylistic-fifteen" -> features.add("'ss15'") - "stylistic-sixteen" -> features.add("'ss16'") - "stylistic-seventeen" -> features.add("'ss17'") - "stylistic-eighteen" -> features.add("'ss18'") - "stylistic-nineteen" -> features.add("'ss19'") - "stylistic-twenty" -> features.add("'ss20'") - } - } - } - fontFeatureSettings = features.joinToString(", ") - } - private fun setFontWeight(fontWeightString: String?) { fontWeight = parseFontWeight(fontWeightString) } @@ -402,6 +355,7 @@ public class TextAttributeProps private constructor() { 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 + public const val TA_KEY_FONT_FEATURE_SETTINGS: Int = 32 private const val TE_KEY_NAME: Int = 0 private const val TE_KEY_PROPS: Int = 1 @@ -438,7 +392,9 @@ public class TextAttributeProps private constructor() { TA_KEY_FONT_SIZE_MULTIPLIER -> {} TA_KEY_FONT_WEIGHT -> result.setFontWeight(entry.stringValue) TA_KEY_FONT_STYLE -> result.setFontStyle(entry.stringValue) - TA_KEY_FONT_VARIANT -> result.setFontVariant(entry.mapBufferValue) + // `fontVariant` is resolved into `TA_KEY_FONT_FEATURE_SETTINGS` by shared C++, so + // `TA_KEY_FONT_VARIANT` carries the unresolved shorthand and is intentionally unused. + TA_KEY_FONT_FEATURE_SETTINGS -> result.fontFeatureSettings = entry.stringValue TA_KEY_FONT_VARIATION_SETTINGS -> result.fontVariationSettings = parseFontVariationSettings(entry.stringValue) TA_KEY_ALLOW_FONT_SCALING -> result.allowFontScaling = entry.booleanValue @@ -507,7 +463,11 @@ public class TextAttributeProps private constructor() { result.fontFamily = getStringProp(props, ViewProps.FONT_FAMILY) result.setFontWeight(getStringProp(props, ViewProps.FONT_WEIGHT)) result.setFontStyle(getStringProp(props, ViewProps.FONT_STYLE)) - result.setFontVariant(getArrayProp(props, ViewProps.FONT_VARIANT)) + result.fontFeatureSettings = + composeFontFeatureSettings( + parseFontVariant(getArrayProp(props, ViewProps.FONT_VARIANT)), + parseFontFeatureSettings(getStringProp(props, ViewProps.FONT_FEATURE_SETTINGS)), + ) result.fontVariationSettings = parseFontVariationSettings(getStringProp(props, ViewProps.FONT_VARIATION_SETTINGS)) result.includeFontPadding = getBooleanProp(props, ViewProps.INCLUDE_FONT_PADDING, true) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt index 66e6f24fbb0..35eb4093198 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt @@ -315,6 +315,7 @@ internal object TextLayoutManager { if ( textAttributes.fontStyle != ReactConstants.UNSET || textAttributes.fontWeight != ReactConstants.UNSET || + textAttributes.fontFeatureSettings != null || textAttributes.fontVariationSettings != null || textAttributes.fontFamily != null ) { @@ -551,6 +552,7 @@ internal object TextLayoutManager { if ( fragment.props.fontStyle != ReactConstants.UNSET || fragment.props.fontWeight != ReactConstants.UNSET || + fragment.props.fontFeatureSettings != null || fragment.props.fontVariationSettings != null || fragment.props.fontFamily != null ) { @@ -893,6 +895,7 @@ internal object TextLayoutManager { if ( baseTextAttributes.fontStyle != ReactConstants.UNSET || baseTextAttributes.fontWeight != ReactConstants.UNSET || + baseTextAttributes.fontFeatureSettings != null || baseTextAttributes.fontVariationSettings != null || baseTextAttributes.fontFamily != null ) { diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextAttributePropsTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextAttributePropsTest.kt index e656463b0f6..233f13dac7f 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextAttributePropsTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextAttributePropsTest.kt @@ -8,6 +8,7 @@ package com.facebook.react.views.text import android.view.Gravity +import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.uimanager.DisplayMetricsHolder import com.facebook.react.uimanager.ReactStylesDiffMap @@ -84,6 +85,115 @@ class TextAttributePropsTest { assertThat(textAlignment("end", isRTL = true)).isEqualTo(Gravity.LEFT) } + @Test + fun readableMapSetsFontFeatureSettings() { + val textAttributes = fromStyles("fontFeatureSettings" to "'ss01', 'zero'") + + assertThat(textAttributes.fontFeatureSettings).isEqualTo("'ss01', 'zero'") + } + + @Test + fun readableMapTrimsFontFeatureSettings() { + val textAttributes = fromStyles("fontFeatureSettings" to " 'ss01' ") + + assertThat(textAttributes.fontFeatureSettings).isEqualTo("'ss01'") + } + + // Per CSS, `normal` means "no author-specified features", not "clear everything". It is also the + // initial value, so on its own it resolves the same as the property never having been set: null, + // which keeps the platform default and keeps the run from taking a span it cannot use. It still + // must not drop the `fontVariant` features composed alongside it, covered below. + @Test + fun readableMapTreatsNormalFontFeatureSettingsAsNoFeatures() { + assertThat(fromStyles("fontFeatureSettings" to "normal").fontFeatureSettings).isNull() + assertThat(fromStyles("fontFeatureSettings" to "NoRmAl").fontFeatureSettings).isNull() + } + + @Test + fun readableMapLeavesFontFeatureSettingsUnsetWhenAbsent() { + assertThat(fromStyles("fontSize" to 16.0).fontFeatureSettings).isNull() + } + + // Golden test for the legacy ReactStylesDiffMap path. This never passes through the shared C++, + // so it composes in Kotlin and must produce the same tags the shared resolution does. + @Test + fun readableMapMapsEveryFontVariantTokenToItsTag() { + val expected = mapOf( + "small-caps" to "'smcp'", + "oldstyle-nums" to "'onum'", + "lining-nums" to "'lnum'", + "tabular-nums" to "'tnum'", + "proportional-nums" to "'pnum'", + "common-ligatures" to "'liga', 'clig'", + "no-common-ligatures" to "'liga' off, 'clig' off", + "discretionary-ligatures" to "'dlig'", + "no-discretionary-ligatures" to "'dlig' off", + "historical-ligatures" to "'hlig'", + "no-historical-ligatures" to "'hlig' off", + "contextual" to "'calt'", + "no-contextual" to "'calt' off", + "stylistic-one" to "'ss01'", + "stylistic-two" to "'ss02'", + "stylistic-three" to "'ss03'", + "stylistic-four" to "'ss04'", + "stylistic-five" to "'ss05'", + "stylistic-six" to "'ss06'", + "stylistic-seven" to "'ss07'", + "stylistic-eight" to "'ss08'", + "stylistic-nine" to "'ss09'", + "stylistic-ten" to "'ss10'", + "stylistic-eleven" to "'ss11'", + "stylistic-twelve" to "'ss12'", + "stylistic-thirteen" to "'ss13'", + "stylistic-fourteen" to "'ss14'", + "stylistic-fifteen" to "'ss15'", + "stylistic-sixteen" to "'ss16'", + "stylistic-seventeen" to "'ss17'", + "stylistic-eighteen" to "'ss18'", + "stylistic-nineteen" to "'ss19'", + "stylistic-twenty" to "'ss20'", + ) + + for ((token, tags) in expected) { + val textAttributes = fromStyles("fontVariant" to JavaOnlyArray.of(token)) + + assertThat(textAttributes.fontFeatureSettings).describedAs(token).isEqualTo(tags) + } + } + + @Test + fun readableMapAppendsFontFeatureSettingsAfterFontVariant() { + val textAttributes = fromStyles( + "fontVariant" to JavaOnlyArray.of("small-caps", "tabular-nums"), + "fontFeatureSettings" to "'smcp' 0", + ) + + assertThat(textAttributes.fontFeatureSettings).isEqualTo("'smcp', 'tnum', 'smcp' 0") + } + + @Test + fun readableMapKeepsFontVariantWhenFontFeatureSettingsIsNormal() { + val textAttributes = fromStyles( + "fontVariant" to JavaOnlyArray.of("small-caps"), + "fontFeatureSettings" to "normal", + ) + + assertThat(textAttributes.fontFeatureSettings).isEqualTo("'smcp'") + } + + private fun fromStyles(vararg styles: Pair): TextAttributeProps { + val map = JavaOnlyMap() + for ((key, value) in styles) { + when (value) { + is String -> map.putString(key, value) + is Double -> map.putDouble(key, value) + is JavaOnlyArray -> map.putArray(key, value) + else -> throw IllegalArgumentException("Unsupported style value: $value") + } + } + return TextAttributeProps.fromReadableMap(ReactStylesDiffMap(map)) + } + private fun textAlignment(textAlign: String, isRTL: Boolean): Int { return TextAttributeProps.getTextAlignment( ReactStylesDiffMap(JavaOnlyMap.of("textAlign", textAlign)), diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp index 5851ec21dff..45f176af3ba 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp @@ -43,6 +43,9 @@ void TextAttributes::apply(TextAttributes textAttributes) { fontVariant = textAttributes.fontVariant.has_value() ? textAttributes.fontVariant : fontVariant; + fontFeatureSettings = textAttributes.fontFeatureSettings.has_value() + ? textAttributes.fontFeatureSettings + : fontFeatureSettings; fontVariationSettings = textAttributes.fontVariationSettings.has_value() ? textAttributes.fontVariationSettings : fontVariationSettings; @@ -124,63 +127,36 @@ void TextAttributes::apply(TextAttributes textAttributes) { #pragma mark - Operators bool TextAttributes::operator==(const TextAttributes& rhs) const { - return std::tie( - foregroundColor, - backgroundColor, - fontFamily, - fontWeight, - fontStyle, - fontVariant, - fontVariationSettings, - allowFontScaling, - dynamicTypeRamp, - alignment, - baseWritingDirection, - lineBreakStrategy, - textDecorationColor, - textDecorationLineType, - textDecorationStyle, - textShadowOffset, - textShadowColor, - isHighlighted, - isPressable, - layoutDirection, - accessibilityRole, - role, - textTransform, - textEffects) == - std::tie( - rhs.foregroundColor, - rhs.backgroundColor, - rhs.fontFamily, - rhs.fontWeight, - rhs.fontStyle, - rhs.fontVariant, - rhs.fontVariationSettings, - rhs.allowFontScaling, - rhs.dynamicTypeRamp, - rhs.alignment, - rhs.baseWritingDirection, - rhs.lineBreakStrategy, - rhs.textDecorationColor, - rhs.textDecorationLineType, - rhs.textDecorationStyle, - rhs.textShadowOffset, - rhs.textShadowColor, - rhs.isHighlighted, - rhs.isPressable, - rhs.layoutDirection, - rhs.accessibilityRole, - rhs.role, - rhs.textTransform, - rhs.textEffects) && + // A short-circuit chain rather than comparing two `std::tie`s, ordered + // cheapest first so that the fields most likely to differ are reached before + // `fontFamily` and `textEffects`, the only two that can reach memory. + return floatEquality(fontSize, rhs.fontSize) && + fontWeight == rhs.fontWeight && fontStyle == rhs.fontStyle && + floatEquality(lineHeight, rhs.lineHeight) && + floatEquality(letterSpacing, rhs.letterSpacing) && + floatEquality(fontSizeMultiplier, rhs.fontSizeMultiplier) && floatEquality(maxFontSizeMultiplier, rhs.maxFontSizeMultiplier) && floatEquality(opacity, rhs.opacity) && - floatEquality(fontSize, rhs.fontSize) && - floatEquality(fontSizeMultiplier, rhs.fontSizeMultiplier) && - floatEquality(letterSpacing, rhs.letterSpacing) && - floatEquality(lineHeight, rhs.lineHeight) && - floatEquality(textShadowRadius, rhs.textShadowRadius); + floatEquality(textShadowRadius, rhs.textShadowRadius) && + foregroundColor == rhs.foregroundColor && + backgroundColor == rhs.backgroundColor && + fontVariant == rhs.fontVariant && + fontFeatureSettings == rhs.fontFeatureSettings && + fontVariationSettings == rhs.fontVariationSettings && + allowFontScaling == rhs.allowFontScaling && + dynamicTypeRamp == rhs.dynamicTypeRamp && alignment == rhs.alignment && + baseWritingDirection == rhs.baseWritingDirection && + lineBreakStrategy == rhs.lineBreakStrategy && + textDecorationColor == rhs.textDecorationColor && + textDecorationLineType == rhs.textDecorationLineType && + textDecorationStyle == rhs.textDecorationStyle && + textShadowOffset == rhs.textShadowOffset && + textShadowColor == rhs.textShadowColor && + isHighlighted == rhs.isHighlighted && isPressable == rhs.isPressable && + layoutDirection == rhs.layoutDirection && + accessibilityRole == rhs.accessibilityRole && role == rhs.role && + textTransform == rhs.textTransform && fontFamily == rhs.fontFamily && + textEffects == rhs.textEffects; } TextAttributes TextAttributes::defaultTextAttributes() { @@ -223,6 +199,10 @@ SharedDebugStringConvertibleList TextAttributes::getDebugProps() const { "fontStyle", fontStyle, textAttributes.fontStyle), debugStringConvertibleItem( "fontVariant", fontVariant, textAttributes.fontVariant), + debugStringConvertibleItem( + "fontFeatureSettings", + fontFeatureSettings, + textAttributes.fontFeatureSettings), debugStringConvertibleItem( "fontVariationSettings", fontVariationSettings, diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h index 683c993bf96..794da82e067 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h @@ -58,6 +58,7 @@ class TextAttributes : public DebugStringConvertible { std::optional fontWeight{}; std::optional fontStyle{}; std::optional fontVariant{}; + std::optional fontFeatureSettings{}; std::optional fontVariationSettings{}; std::optional allowFontScaling{}; Float maxFontSizeMultiplier{std::numeric_limits::quiet_NaN()}; @@ -133,7 +134,11 @@ struct hash { for (const auto &effect : textAttributes.textEffects) { facebook::react::hash_combine(textEffectsHash, effect); } - return facebook::react::hash_combine( + // The optionals are folded into one presence mask rather than being chained individually, so + // that the properties a given text run does not set stay off the hash's dependency chain. + size_t seed = 0; + facebook::react::hash_combine( + seed, textAttributes.foregroundColor, textAttributes.backgroundColor, textAttributes.opacity, @@ -141,30 +146,34 @@ struct hash { textAttributes.fontSize, textAttributes.maxFontSizeMultiplier, textAttributes.fontSizeMultiplier, + textAttributes.letterSpacing, + textAttributes.lineHeight, + textAttributes.textShadowRadius, + textAttributes.textDecorationColor, + textAttributes.textShadowColor, + textEffectsHash); + facebook::react::hash_combine_optionals( + seed, textAttributes.fontWeight, textAttributes.fontStyle, textAttributes.fontVariant, + textAttributes.fontFeatureSettings, textAttributes.fontVariationSettings, textAttributes.allowFontScaling, - textAttributes.letterSpacing, textAttributes.textTransform, - textAttributes.lineHeight, textAttributes.alignment, textAttributes.baseWritingDirection, textAttributes.lineBreakStrategy, textAttributes.lineBreakMode, - textAttributes.textDecorationColor, textAttributes.textDecorationLineType, textAttributes.textDecorationStyle, textAttributes.textShadowOffset, - textAttributes.textShadowRadius, - textAttributes.textShadowColor, textAttributes.isHighlighted, textAttributes.isPressable, textAttributes.layoutDirection, textAttributes.accessibilityRole, - textAttributes.role, - textEffectsHash); + textAttributes.role); + return seed; } }; } // namespace std diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h index beed9673780..e590be3607e 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h @@ -21,9 +21,16 @@ #include #include #include +#include #include #include +#include +#include +#include +#include +#include #include +#include #ifdef RN_SERIALIZABLE_STATE #include @@ -573,6 +580,113 @@ inline std::string toString(const FontVariant &fontVariant) return result; } +/* + * Bits are tested in the same order `toMapBuffer(const FontVariant &)` + * serializes them, so the composed list is byte-identical to what the platforms + * previously derived from that MapBuffer themselves. + * + * Only 25 of the 33 `fontVariant` values the JS types accept appear here, + * because `FontVariant` has no bits for the eight ligature and contextual + * values (see `fontVariantFromCSSFontVariant`). Those are dropped before + * reaching this point; widening the enum is tracked separately. + */ +inline std::string fontVariantToOpenTypeFeatures(const FontVariant &fontVariant) +{ + static constexpr std::array, 25> featureTags{{ + {FontVariant::SmallCaps, "'smcp'"}, {FontVariant::OldstyleNums, "'onum'"}, + {FontVariant::LiningNums, "'lnum'"}, {FontVariant::TabularNums, "'tnum'"}, + {FontVariant::ProportionalNums, "'pnum'"}, {FontVariant::StylisticOne, "'ss01'"}, + {FontVariant::StylisticTwo, "'ss02'"}, {FontVariant::StylisticThree, "'ss03'"}, + {FontVariant::StylisticFour, "'ss04'"}, {FontVariant::StylisticFive, "'ss05'"}, + {FontVariant::StylisticSix, "'ss06'"}, {FontVariant::StylisticSeven, "'ss07'"}, + {FontVariant::StylisticEight, "'ss08'"}, {FontVariant::StylisticNine, "'ss09'"}, + {FontVariant::StylisticTen, "'ss10'"}, {FontVariant::StylisticEleven, "'ss11'"}, + {FontVariant::StylisticTwelve, "'ss12'"}, {FontVariant::StylisticThirteen, "'ss13'"}, + {FontVariant::StylisticFourteen, "'ss14'"}, {FontVariant::StylisticFifteen, "'ss15'"}, + {FontVariant::StylisticSixteen, "'ss16'"}, {FontVariant::StylisticSeventeen, "'ss17'"}, + {FontVariant::StylisticEighteen, "'ss18'"}, {FontVariant::StylisticNineteen, "'ss19'"}, + {FontVariant::StylisticTwenty, "'ss20'"}, + }}; + + auto result = std::string{}; + for (const auto &[bit, tag] : featureTags) { + if (((int)fontVariant & (int)bit) != 0) { + if (!result.empty()) { + result += ", "; + } + result += tag; + } + } + + return result; +} + +/* + * Resolves the OpenType feature list a text run renders with, combining the + * `fontVariant` shorthand and the high-level longhands with the author's raw + * `fontFeatureSettings`. + * + * Contributions are emitted least- to most-specific, so a tag named more than + * once resolves last-wins in the CSS grammar and in both platform shapers and + * no explicit conflict resolution is needed. `fontFeatureSettings` is last + * because CSS makes it the low-level escape hatch that overrides the + * high-level properties. + * + * Platforms that apply `fontVariant` themselves pass `std::nullopt` for it and + * still get everything else. + * + * Returns `std::nullopt` when nothing is set and equally when what is set + * contributes no feature, keeping the property absent from serialization so the + * platform falls through to its own default. Per CSS, `normal` contributes no + * features rather than clearing what came before it, and it is the initial + * value, so an explicit one has to resolve the same as never having set it. + */ +inline std::optional resolveFontFeatureSettings( + const TextAttributes &textAttributes, + bool includeFontVariant = true) +{ + static const std::optional noFontVariant{}; + const auto &fontVariant = includeFontVariant ? textAttributes.fontVariant : noFontVariant; + const auto &fontFeatureSettings = textAttributes.fontFeatureSettings; + + if (!fontVariant.has_value() && !fontFeatureSettings.has_value()) { + return std::nullopt; + } + + auto result = fontVariant.has_value() ? fontVariantToOpenTypeFeatures(*fontVariant) : std::string{}; + + if (fontFeatureSettings.has_value()) { + auto value = std::string_view{*fontFeatureSettings}; + auto begin = value.find_first_not_of(" \t\n\r\f\v"); + if (begin != std::string_view::npos) { + value = value.substr(begin, value.find_last_not_of(" \t\n\r\f\v") - begin + 1); + } else { + value = {}; + } + + constexpr std::string_view kNormal{"normal"}; + auto isNormal = value.size() == kNormal.size(); + for (size_t i = 0; isNormal && i < kNormal.size(); i++) { + isNormal = (char)std::tolower((unsigned char)value[i]) == kNormal[i]; + } + + if (!value.empty() && !isNormal) { + if (!result.empty()) { + result += ", "; + } + result += value; + } + } + + // Set but contributing nothing, such as a lone `normal`, is not the same as + // asking for an empty feature list: the platform default has to survive. + if (result.empty()) { + return std::nullopt; + } + + return result; +} + inline void fromRawValue(const PropsParserContext &context, const RawValue &value, TextTransform &result) { react_native_expect(value.hasType()); @@ -1143,6 +1257,7 @@ constexpr static MapBuffer::Key TA_KEY_ALIGNMENT_VERTICAL = 28; constexpr static MapBuffer::Key TA_KEY_MAX_FONT_SIZE_MULTIPLIER = 29; constexpr static MapBuffer::Key TA_KEY_TEXT_EFFECTS = 30; constexpr static MapBuffer::Key TA_KEY_FONT_VARIATION_SETTINGS = 31; +constexpr static MapBuffer::Key TA_KEY_FONT_FEATURE_SETTINGS = 32; // Keys within each text effect entry MapBuffer constexpr static MapBuffer::Key TE_KEY_NAME = 0; @@ -1291,6 +1406,12 @@ inline MapBuffer toMapBuffer(const TextAttributes &textAttributes) auto fontVariantMap = toMapBuffer(*textAttributes.fontVariant); builder.putMapBuffer(TA_KEY_FONT_VARIANT, fontVariantMap); } + // Composed here rather than on the platform so both renderers agree on how + // `fontVariant` and `fontFeatureSettings` combine. `TA_KEY_FONT_VARIANT` is + // still emitted for consumers that read the raw shorthand. + if (auto fontFeatureSettings = resolveFontFeatureSettings(textAttributes)) { + builder.putString(TA_KEY_FONT_FEATURE_SETTINGS, *fontFeatureSettings); + } if (textAttributes.fontVariationSettings.has_value()) { builder.putString(TA_KEY_FONT_VARIATION_SETTINGS, *textAttributes.fontVariationSettings); } diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/primitives.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/primitives.h index 37ad9265edc..40433787448 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/primitives.h @@ -10,6 +10,8 @@ #include #include +#include + namespace facebook::react { enum class FontStyle { Normal, Italic, Oblique }; diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/tests/FontFeatureSettingsTest.cpp b/packages/react-native/ReactCommon/react/renderer/attributedstring/tests/FontFeatureSettingsTest.cpp new file mode 100644 index 00000000000..e6652978e08 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/tests/FontFeatureSettingsTest.cpp @@ -0,0 +1,201 @@ +/* + * 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. + */ + +#include + +#include + +#include +#include + +using namespace facebook::react; + +namespace { + +FontVariant variants(std::initializer_list values) { + auto result = 0; + for (auto value : values) { + result |= (int)value; + } + return (FontVariant)result; +} + +TextAttributes attributesWith( + std::optional fontVariant = std::nullopt, + std::optional fontFeatureSettings = std::nullopt) { + TextAttributes textAttributes; + textAttributes.fontVariant = fontVariant; + textAttributes.fontFeatureSettings = std::move(fontFeatureSettings); + return textAttributes; +} + +std::optional resolve( + std::optional fontVariant = std::nullopt, + std::optional fontFeatureSettings = std::nullopt) { + auto textAttributes = + attributesWith(fontVariant, std::move(fontFeatureSettings)); + return resolveFontFeatureSettings(textAttributes); +} + +} // namespace + +TEST(FontFeatureSettingsTest, everyFontVariantBitMapsToItsHistoricalTag) { + const auto expected = std::to_array>({ + {FontVariant::SmallCaps, "'smcp'"}, + {FontVariant::OldstyleNums, "'onum'"}, + {FontVariant::LiningNums, "'lnum'"}, + {FontVariant::TabularNums, "'tnum'"}, + {FontVariant::ProportionalNums, "'pnum'"}, + {FontVariant::StylisticOne, "'ss01'"}, + {FontVariant::StylisticTwo, "'ss02'"}, + {FontVariant::StylisticThree, "'ss03'"}, + {FontVariant::StylisticFour, "'ss04'"}, + {FontVariant::StylisticFive, "'ss05'"}, + {FontVariant::StylisticSix, "'ss06'"}, + {FontVariant::StylisticSeven, "'ss07'"}, + {FontVariant::StylisticEight, "'ss08'"}, + {FontVariant::StylisticNine, "'ss09'"}, + {FontVariant::StylisticTen, "'ss10'"}, + {FontVariant::StylisticEleven, "'ss11'"}, + {FontVariant::StylisticTwelve, "'ss12'"}, + {FontVariant::StylisticThirteen, "'ss13'"}, + {FontVariant::StylisticFourteen, "'ss14'"}, + {FontVariant::StylisticFifteen, "'ss15'"}, + {FontVariant::StylisticSixteen, "'ss16'"}, + {FontVariant::StylisticSeventeen, "'ss17'"}, + {FontVariant::StylisticEighteen, "'ss18'"}, + {FontVariant::StylisticNineteen, "'ss19'"}, + {FontVariant::StylisticTwenty, "'ss20'"}, + }); + + for (const auto& [variant, tag] : expected) { + EXPECT_EQ(fontVariantToOpenTypeFeatures(variant), tag); + } +} + +TEST(FontFeatureSettingsTest, defaultFontVariantProducesNoTags) { + EXPECT_EQ(fontVariantToOpenTypeFeatures(FontVariant::Default), ""); +} + +// `toMapBuffer(const FontVariant &)` serializes in bit order, so the platforms +// were already receiving bit-ordered tokens rather than authoring order. Pin +// that, since it is what makes this composition byte-identical to the old one. +TEST(FontFeatureSettingsTest, multipleVariantsJoinInBitOrder) { + EXPECT_EQ( + fontVariantToOpenTypeFeatures(variants( + {FontVariant::StylisticThree, + FontVariant::SmallCaps, + FontVariant::TabularNums})), + "'smcp', 'tnum', 'ss03'"); +} + +#pragma mark - Resolution against fontFeatureSettings + +// `std::nullopt` keeps the key out of serialization entirely, so the platform +// keeps its own default. +TEST(FontFeatureSettingsTest, bothUnsetResolvesToNullopt) { + EXPECT_FALSE(resolve(std::nullopt, std::nullopt)); +} + +TEST(FontFeatureSettingsTest, fontVariantAloneResolvesToItsTags) { + EXPECT_EQ(resolve(FontVariant::SmallCaps, std::nullopt), "'smcp'"); +} + +TEST(FontFeatureSettingsTest, fontFeatureSettingsAloneIsForwardedVerbatim) { + EXPECT_EQ(resolve(std::nullopt, "'ss01' 1, 'zero'"), "'ss01' 1, 'zero'"); +} + +// The precedence contract: `fontFeatureSettings` is appended last, and both the +// CSS grammar and the platform shapers resolve duplicate tags last-wins. So +// ordering alone gives `fontFeatureSettings` the win, with no explicit conflict +// resolution to keep in sync between platforms. +TEST(FontFeatureSettingsTest, fontFeatureSettingsIsAppendedAfterFontVariant) { + EXPECT_EQ( + resolve( + variants({FontVariant::SmallCaps, FontVariant::TabularNums}), + "'smcp' 0"), + "'smcp', 'tnum', 'smcp' 0"); +} + +TEST(FontFeatureSettingsTest, normalContributesNoFeaturesButKeepsFontVariant) { + EXPECT_EQ(resolve(FontVariant::SmallCaps, "normal"), "'smcp'"); + EXPECT_FALSE(resolve(std::nullopt, "normal")); +} + +TEST(FontFeatureSettingsTest, normalIsMatchedCaseInsensitivelyAndTrimmed) { + for (const auto* value : {"NoRmAl", " normal ", "\tNORMAL\n"}) { + EXPECT_FALSE(resolve(std::nullopt, value)) << "value: " << value; + } +} + +TEST(FontFeatureSettingsTest, emptyStringContributesNoFeatures) { + EXPECT_EQ(resolve(FontVariant::SmallCaps, ""), "'smcp'"); + EXPECT_FALSE(resolve(std::nullopt, "")); +} + +TEST(FontFeatureSettingsTest, surroundingWhitespaceIsTrimmedFromRealValues) { + EXPECT_EQ(resolve(std::nullopt, " 'ss01' "), "'ss01'"); +} + +// `normal` is the initial value of the property, so an explicit one renders the +// same as never having set it: it names no feature, and it does not switch off +// the features the font applies on its own. Resolving it to an engaged empty +// string would serialize a value that says "no features" where the contract is +// "no opinion", which costs a span on Android that carries nothing. +TEST(FontFeatureSettingsTest, explicitResetResolvesTheSameAsUnset) { + EXPECT_EQ( + resolve(std::nullopt, "normal"), resolve(std::nullopt, std::nullopt)); +} + +#pragma mark - Inheritance + +TEST(FontFeatureSettingsTest, childFontFeatureSettingsReplacesParent) { + TextAttributes parent; + TextAttributes child; + + parent.fontFeatureSettings = "'ss01'"; + child.fontFeatureSettings = "'ss02'"; + parent.apply(child); + + EXPECT_EQ(parent.fontFeatureSettings, "'ss02'"); +} + +TEST(FontFeatureSettingsTest, unsetChildInheritsParentFontFeatureSettings) { + TextAttributes parent; + TextAttributes child; + + parent.fontFeatureSettings = "'ss01'"; + parent.apply(child); + + EXPECT_EQ(parent.fontFeatureSettings, "'ss01'"); +} + +TEST(FontFeatureSettingsTest, emptyChildClearsInheritedFontFeatureSettings) { + TextAttributes parent; + TextAttributes child; + + parent.fontFeatureSettings = "'ss01'"; + child.fontFeatureSettings = ""; + parent.apply(child); + + ASSERT_TRUE(parent.fontFeatureSettings.has_value()); + EXPECT_TRUE(parent.fontFeatureSettings->empty()); +} + +// `fontVariant` and `fontFeatureSettings` inherit independently, so a child +// overriding one must not discard the other. +TEST(FontFeatureSettingsTest, fontVariantAndFontFeatureSettingsInheritApart) { + TextAttributes parent; + TextAttributes child; + + parent.fontVariant = FontVariant::SmallCaps; + child.fontFeatureSettings = "'ss01'"; + parent.apply(child); + + EXPECT_EQ(parent.fontVariant, FontVariant::SmallCaps); + EXPECT_EQ(resolveFontFeatureSettings(parent), "'smcp', 'ss01'"); +} diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/tests/TextAttributesHashTest.cpp b/packages/react-native/ReactCommon/react/renderer/attributedstring/tests/TextAttributesHashTest.cpp new file mode 100644 index 00000000000..4a6f5c807fc --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/tests/TextAttributesHashTest.cpp @@ -0,0 +1,94 @@ +/* + * 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. + */ + +#include + +#include + +#include + +namespace facebook::react { +namespace { + +template +void expectOptionalFieldAffectsHash( + const char* fieldName, + std::optional TextAttributes::* field) { + SCOPED_TRACE(fieldName); + TextAttributes baseline; + TextAttributes changed; + changed.*field = T{}; + + EXPECT_NE( + std::hash{}(baseline), + std::hash{}(changed)); +} + +} // namespace + +TEST(TextAttributesHashTest, equalAttributesHaveEqualHashes) { + TextAttributes lhs; + lhs.fontWeight = FontWeight::Weight400; + lhs.fontStyle = FontStyle::Italic; + lhs.fontVariationSettings = "'wght' 400"; + lhs.allowFontScaling = true; + lhs.alignment = TextAlignment::Center; + lhs.textShadowOffset = Size{.width = 1, .height = 2}; + lhs.isPressable = true; + + TextAttributes rhs = lhs; + + EXPECT_EQ(lhs, rhs); + EXPECT_EQ(std::hash{}(lhs), std::hash{}(rhs)); +} + +TEST(TextAttributesHashTest, everyOptionalHashedFieldAffectsHash) { + expectOptionalFieldAffectsHash("fontWeight", &TextAttributes::fontWeight); + expectOptionalFieldAffectsHash("fontStyle", &TextAttributes::fontStyle); + expectOptionalFieldAffectsHash("fontVariant", &TextAttributes::fontVariant); + expectOptionalFieldAffectsHash( + "fontVariationSettings", &TextAttributes::fontVariationSettings); + expectOptionalFieldAffectsHash( + "allowFontScaling", &TextAttributes::allowFontScaling); + expectOptionalFieldAffectsHash( + "textTransform", &TextAttributes::textTransform); + expectOptionalFieldAffectsHash("alignment", &TextAttributes::alignment); + expectOptionalFieldAffectsHash( + "baseWritingDirection", &TextAttributes::baseWritingDirection); + expectOptionalFieldAffectsHash( + "lineBreakStrategy", &TextAttributes::lineBreakStrategy); + expectOptionalFieldAffectsHash( + "lineBreakMode", &TextAttributes::lineBreakMode); + expectOptionalFieldAffectsHash( + "textDecorationLineType", &TextAttributes::textDecorationLineType); + expectOptionalFieldAffectsHash( + "textDecorationStyle", &TextAttributes::textDecorationStyle); + expectOptionalFieldAffectsHash( + "textShadowOffset", &TextAttributes::textShadowOffset); + expectOptionalFieldAffectsHash( + "isHighlighted", &TextAttributes::isHighlighted); + expectOptionalFieldAffectsHash("isPressable", &TextAttributes::isPressable); + expectOptionalFieldAffectsHash( + "layoutDirection", &TextAttributes::layoutDirection); + expectOptionalFieldAffectsHash( + "accessibilityRole", &TextAttributes::accessibilityRole); + expectOptionalFieldAffectsHash("role", &TextAttributes::role); +} + +TEST(TextAttributesHashTest, identicalValuesInDifferentSlotsHashDifferently) { + TextAttributes highlighted; + highlighted.isHighlighted = true; + + TextAttributes pressable; + pressable.isPressable = true; + + EXPECT_NE( + std::hash{}(highlighted), + std::hash{}(pressable)); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp index 135463aa2d0..0bb72eccc75 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp @@ -67,6 +67,12 @@ static TextAttributes convertRawProp( "fontVariant", sourceTextAttributes.fontVariant, defaultTextAttributes.fontVariant); + textAttributes.fontFeatureSettings = convertRawProp( + context, + rawProps, + "fontFeatureSettings", + sourceTextAttributes.fontFeatureSettings, + defaultTextAttributes.fontFeatureSettings); textAttributes.fontVariationSettings = convertRawProp( context, rawProps, @@ -267,6 +273,12 @@ void BaseTextProps::setProp( defaults, value, textAttributes, fontStyle, "fontStyle"); REBUILD_FIELD_SWITCH_CASE( defaults, value, textAttributes, fontVariant, "fontVariant"); + REBUILD_FIELD_SWITCH_CASE( + defaults, + value, + textAttributes, + fontFeatureSettings, + "fontFeatureSettings"); REBUILD_FIELD_SWITCH_CASE( defaults, value, @@ -413,6 +425,14 @@ void BaseTextProps::appendTextAttributesProps( : folly::dynamic(nullptr); } + if (textAttributes.fontFeatureSettings != + oldProps->textAttributes.fontFeatureSettings) { + result["fontFeatureSettings"] = + textAttributes.fontFeatureSettings.has_value() + ? folly::dynamic(*textAttributes.fontFeatureSettings) + : folly::dynamic(nullptr); + } + if (textAttributes.allowFontScaling != oldProps->textAttributes.allowFontScaling) { result["allowFontScaling"] = textAttributes.allowFontScaling.has_value() diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/tests/BaseTextShadowNodeTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/tests/BaseTextShadowNodeTest.cpp index 0d4ad930c52..3e0a0631de8 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/tests/BaseTextShadowNodeTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/tests/BaseTextShadowNodeTest.cpp @@ -58,12 +58,14 @@ TextAttributes nestedTextAttributes( return output.getFragments()[0].textAttributes; } -std::shared_ptr textPropsWithRawFontVariationSettings( - const char* fontVariationSettings) { +// Builds props through the real raw-prop parse path rather than assigning the +// field directly, so the test exercises the same conversion the runtime does. +std::shared_ptr textPropsWithRawProp( + const char* name, + const char* value) { ContextContainer contextContainer{}; PropsParserContext parserContext{-1, contextContainer}; - auto rawProps = RawProps( - folly::dynamic::object("fontVariationSettings", fontVariationSettings)); + auto rawProps = RawProps(folly::dynamic::object(name, value)); auto rawPropsParser = RawPropsParser{}; rawPropsParser.prepare(); rawProps.parse(rawPropsParser); @@ -158,7 +160,7 @@ TEST(BaseTextShadowNodeTest, childInheritsParentFontVariationSettings) { TEST(BaseTextShadowNodeTest, childFontVariationSettingsReplaceParentList) { auto parentProps = std::make_shared(); parentProps->textAttributes.fontVariationSettings = "'wght' 650, 'wdth' 90"; - auto childProps = textPropsWithRawFontVariationSettings("'wdth' 110"); + auto childProps = textPropsWithRawProp("fontVariationSettings", "'wdth' 110"); const auto attributes = nestedTextAttributes(parentProps, childProps); @@ -179,4 +181,39 @@ TEST( EXPECT_EQ(attributes.fontVariationSettings, "'wght' 650"); } +TEST(BaseTextShadowNodeTest, childInheritsParentFontFeatureSettings) { + auto parentProps = std::make_shared(); + parentProps->textAttributes.fontFeatureSettings = "'ss01'"; + auto childProps = std::make_shared(); + + const auto attributes = nestedTextAttributes(parentProps, childProps); + + EXPECT_EQ(attributes.fontFeatureSettings, "'ss01'"); +} + +TEST(BaseTextShadowNodeTest, childFontFeatureSettingsReplaceParentList) { + auto parentProps = std::make_shared(); + parentProps->textAttributes.fontFeatureSettings = "'ss01', 'onum'"; + auto childProps = textPropsWithRawProp("fontFeatureSettings", "'tnum'"); + + const auto attributes = nestedTextAttributes(parentProps, childProps); + + EXPECT_EQ(attributes.fontFeatureSettings, "'tnum'"); +} + +// The two properties compose rather than override, so a child setting one must +// still resolve against the other inherited from the parent. +TEST( + BaseTextShadowNodeTest, + childFontFeatureSettingsComposeWithInheritedFontVariant) { + auto parentProps = std::make_shared(); + parentProps->textAttributes.fontVariant = FontVariant::SmallCaps; + auto childProps = textPropsWithRawProp("fontFeatureSettings", "'ss01'"); + + const auto attributes = nestedTextAttributes(parentProps, childProps); + + EXPECT_EQ(attributes.fontVariant, FontVariant::SmallCaps); + EXPECT_EQ(resolveFontFeatureSettings(attributes), "'smcp', 'ss01'"); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h index e302e5f7dde..e8f180b7610 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h @@ -125,6 +125,7 @@ inline bool areTextAttributesEquivalentLayoutWise(const TextAttributes &lhs, con lhs.fontWeight, lhs.fontStyle, lhs.fontVariant, + lhs.fontFeatureSettings, lhs.fontVariationSettings, lhs.allowFontScaling, lhs.dynamicTypeRamp, @@ -134,6 +135,7 @@ inline bool areTextAttributesEquivalentLayoutWise(const TextAttributes &lhs, con rhs.fontWeight, rhs.fontStyle, rhs.fontVariant, + rhs.fontFeatureSettings, rhs.fontVariationSettings, rhs.allowFontScaling, rhs.dynamicTypeRamp, @@ -147,20 +149,29 @@ inline size_t textAttributesHashLayoutWise(const TextAttributes &textAttributes) { // Taking into account the same props as // `areTextAttributesEquivalentLayoutWise` mentions. - return facebook::react::hash_combine( + // The optionals are folded into one presence mask rather than being chained individually, so + // that the properties a given text run does not set stay off the hash's dependency chain. This + // is the hot path: it runs on every measurement-cache probe. + size_t seed = 0; + facebook::react::hash_combine( + seed, textAttributes.fontFamily, textAttributes.fontSize, textAttributes.fontSizeMultiplier, + textAttributes.maxFontSizeMultiplier, + textAttributes.letterSpacing, + textAttributes.lineHeight); + facebook::react::hash_combine_optionals( + seed, textAttributes.fontWeight, textAttributes.fontStyle, textAttributes.fontVariant, + textAttributes.fontFeatureSettings, textAttributes.fontVariationSettings, textAttributes.allowFontScaling, - textAttributes.maxFontSizeMultiplier, textAttributes.dynamicTypeRamp, - textAttributes.letterSpacing, - textAttributes.lineHeight, textAttributes.alignment); + return seed; } inline bool areAttributedStringFragmentsEquivalentLayoutWise( diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm index 95ff5a0d180..4c3f3740d36 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm @@ -8,6 +8,7 @@ #import "RCTAttributedTextUtils.h" #include +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include using namespace facebook::react; diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/tests/TextLayoutManagerTest.cpp b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/tests/TextLayoutManagerTest.cpp index 3b687657917..f1baa35d658 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/tests/TextLayoutManagerTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/tests/TextLayoutManagerTest.cpp @@ -7,6 +7,7 @@ #include +#include #include #include @@ -69,6 +70,27 @@ TEST(TextLayoutManagerTest, fontVariationSettingsAffectLayoutCacheHash) { textAttributesHashLayoutWise(lhs), textAttributesHashLayoutWise(rhs)); } +TEST(TextLayoutManagerTest, fontFeatureSettingsAffectLayoutCacheEquality) { + TextAttributes lhs; + TextAttributes rhs; + + lhs.fontFeatureSettings = "'tnum'"; + rhs.fontFeatureSettings = "'pnum'"; + + EXPECT_FALSE(areTextAttributesEquivalentLayoutWise(lhs, rhs)); +} + +TEST(TextLayoutManagerTest, fontFeatureSettingsAffectLayoutCacheHash) { + TextAttributes lhs; + TextAttributes rhs; + + lhs.fontFeatureSettings = "'tnum'"; + rhs.fontFeatureSettings = "'pnum'"; + + EXPECT_NE( + textAttributesHashLayoutWise(lhs), textAttributesHashLayoutWise(rhs)); +} + TEST(TextLayoutManagerTest, emptyFontVariationSettingsClearInheritedSettings) { TextAttributes parent; TextAttributes child; @@ -81,6 +103,122 @@ TEST(TextLayoutManagerTest, emptyFontVariationSettingsClearInheritedSettings) { EXPECT_TRUE(parent.fontVariationSettings->empty()); } +TEST(TextLayoutManagerTest, everyLayoutAttributeAffectsEqualityAndHash) { + TextAttributes baseline; + baseline.fontFamily = "Inter"; + baseline.fontSize = 16; + baseline.fontSizeMultiplier = 1; + baseline.maxFontSizeMultiplier = 2; + baseline.letterSpacing = 0.5; + baseline.lineHeight = 20; + baseline.fontWeight = FontWeight::Weight400; + baseline.fontStyle = FontStyle::Normal; + baseline.fontVariant = FontVariant::Default; + baseline.fontVariationSettings = "'wght' 400"; + baseline.allowFontScaling = true; + baseline.dynamicTypeRamp = DynamicTypeRamp::Body; + baseline.alignment = TextAlignment::Natural; + + struct TestCase { + const char* name; + void (*mutate)(TextAttributes&); + }; + const std::array testCases{{ + {.name = "fontFamily", + .mutate = + [](TextAttributes& attributes) { + attributes.fontFamily = "Roboto"; + }}, + {.name = "fontSize", + .mutate = [](TextAttributes& attributes) { attributes.fontSize = 17; }}, + {.name = "fontSizeMultiplier", + .mutate = + [](TextAttributes& attributes) { + attributes.fontSizeMultiplier = 1.5; + }}, + {.name = "maxFontSizeMultiplier", + .mutate = + [](TextAttributes& attributes) { + attributes.maxFontSizeMultiplier = 3; + }}, + {.name = "letterSpacing", + .mutate = + [](TextAttributes& attributes) { attributes.letterSpacing = 1; }}, + {.name = "lineHeight", + .mutate = + [](TextAttributes& attributes) { attributes.lineHeight = 24; }}, + {.name = "fontWeight", + .mutate = + [](TextAttributes& attributes) { + attributes.fontWeight = FontWeight::Weight700; + }}, + {.name = "fontStyle", + .mutate = + [](TextAttributes& attributes) { + attributes.fontStyle = FontStyle::Italic; + }}, + {.name = "fontVariant", + .mutate = + [](TextAttributes& attributes) { + attributes.fontVariant = FontVariant::SmallCaps; + }}, + {.name = "fontVariationSettings", + .mutate = + [](TextAttributes& attributes) { + attributes.fontVariationSettings = "'wght' 700"; + }}, + {.name = "allowFontScaling", + .mutate = + [](TextAttributes& attributes) { + attributes.allowFontScaling = false; + }}, + {.name = "dynamicTypeRamp", + .mutate = + [](TextAttributes& attributes) { + attributes.dynamicTypeRamp = DynamicTypeRamp::Headline; + }}, + {.name = "alignment", + .mutate = + [](TextAttributes& attributes) { + attributes.alignment = TextAlignment::Center; + }}, + }}; + + for (const auto& testCase : testCases) { + SCOPED_TRACE(testCase.name); + auto changed = baseline; + testCase.mutate(changed); + + EXPECT_FALSE(areTextAttributesEquivalentLayoutWise(baseline, changed)); + EXPECT_NE( + textAttributesHashLayoutWise(baseline), + textAttributesHashLayoutWise(changed)); + } +} + +TEST(TextLayoutManagerTest, equivalentPopulatedLayoutAttributesHashEqually) { + TextAttributes lhs; + lhs.fontFamily = "Inter"; + lhs.fontSize = 16; + lhs.fontSizeMultiplier = 1; + lhs.maxFontSizeMultiplier = 2; + lhs.letterSpacing = 0.5; + lhs.lineHeight = 20; + lhs.fontWeight = FontWeight::Weight400; + lhs.fontStyle = FontStyle::Italic; + lhs.fontVariant = FontVariant::SmallCaps; + lhs.fontVariationSettings = "'wght' 400"; + lhs.allowFontScaling = true; + lhs.dynamicTypeRamp = DynamicTypeRamp::Body; + lhs.alignment = TextAlignment::Center; + + auto rhs = lhs; + + EXPECT_TRUE(areTextAttributesEquivalentLayoutWise(lhs, rhs)); + EXPECT_EQ( + textAttributesHashLayoutWise(lhs), textAttributesHashLayoutWise(rhs)); +} + // Measurements are rounded to the pixel grid, so a measurement cached at one // pixel scale factor must not satisfy a lookup at another. Keys that differ // only by pointScaleFactor must compare unequal. diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/tests/benchmarks/TextAttributesHashBenchmark.cpp b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/tests/benchmarks/TextAttributesHashBenchmark.cpp new file mode 100644 index 00000000000..0f2a2b5ebe4 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/tests/benchmarks/TextAttributesHashBenchmark.cpp @@ -0,0 +1,252 @@ +/* + * 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. + */ + +#include + +#include +#include + +namespace facebook::react { +namespace { + +size_t legacyTextAttributesHash(const TextAttributes& textAttributes) { + size_t textEffectsHash = 0; + for (const auto& effect : textAttributes.textEffects) { + hash_combine(textEffectsHash, effect); + } + return hash_combine( + textAttributes.foregroundColor, + textAttributes.backgroundColor, + textAttributes.opacity, + textAttributes.fontFamily, + textAttributes.fontSize, + textAttributes.maxFontSizeMultiplier, + textAttributes.fontSizeMultiplier, + textAttributes.fontWeight, + textAttributes.fontStyle, + textAttributes.fontVariant, + textAttributes.fontVariationSettings, + textAttributes.allowFontScaling, + textAttributes.letterSpacing, + textAttributes.textTransform, + textAttributes.lineHeight, + textAttributes.alignment, + textAttributes.baseWritingDirection, + textAttributes.lineBreakStrategy, + textAttributes.lineBreakMode, + textAttributes.textDecorationColor, + textAttributes.textDecorationLineType, + textAttributes.textDecorationStyle, + textAttributes.textShadowOffset, + textAttributes.textShadowRadius, + textAttributes.textShadowColor, + textAttributes.isHighlighted, + textAttributes.isPressable, + textAttributes.layoutDirection, + textAttributes.accessibilityRole, + textAttributes.role, + textEffectsHash); +} + +size_t legacyLayoutHash(const TextAttributes& textAttributes) { + return hash_combine( + textAttributes.fontFamily, + textAttributes.fontSize, + textAttributes.fontSizeMultiplier, + textAttributes.fontWeight, + textAttributes.fontStyle, + textAttributes.fontVariant, + textAttributes.fontVariationSettings, + textAttributes.allowFontScaling, + textAttributes.maxFontSizeMultiplier, + textAttributes.dynamicTypeRamp, + textAttributes.letterSpacing, + textAttributes.lineHeight, + textAttributes.alignment); +} + +TextAttributes sparseAttributes() { + TextAttributes attributes; + attributes.fontWeight = FontWeight::Weight400; + return attributes; +} + +TextAttributes partialAttributes() { + TextAttributes attributes; + attributes.fontFamily = "Inter"; + attributes.fontSize = 16; + attributes.fontSizeMultiplier = 1; + attributes.maxFontSizeMultiplier = 2; + attributes.letterSpacing = 0.5; + attributes.lineHeight = 20; + attributes.fontWeight = FontWeight::Weight400; + attributes.fontStyle = FontStyle::Normal; + attributes.allowFontScaling = true; + attributes.alignment = TextAlignment::Natural; + return attributes; +} + +TextAttributes fullAttributes() { + auto attributes = partialAttributes(); + attributes.fontVariant = FontVariant::SmallCaps; + attributes.fontVariationSettings = "'wght' 400"; + attributes.dynamicTypeRamp = DynamicTypeRamp::Body; + attributes.textTransform = TextTransform::None; + attributes.baseWritingDirection = WritingDirection::LeftToRight; + attributes.lineBreakStrategy = LineBreakStrategy::Standard; + attributes.lineBreakMode = LineBreakMode::Word; + attributes.textDecorationLineType = TextDecorationLineType::Underline; + attributes.textDecorationStyle = TextDecorationStyle::Solid; + attributes.textShadowOffset = Size{.width = 1, .height = 1}; + attributes.isHighlighted = false; + attributes.isPressable = true; + attributes.layoutDirection = LayoutDirection{}; + attributes.accessibilityRole = AccessibilityRole{}; + attributes.role = Role{}; + return attributes; +} + +struct DefaultAttributes { + static TextAttributes make() { + return {}; + } +}; + +struct SparseAttributes { + static TextAttributes make() { + return sparseAttributes(); + } +}; + +struct PartialAttributes { + static TextAttributes make() { + return partialAttributes(); + } +}; + +struct FullAttributes { + static TextAttributes make() { + return fullAttributes(); + } +}; + +struct LegacyFullHash { + static size_t hash(const TextAttributes& attributes) { + return legacyTextAttributesHash(attributes); + } +}; + +struct OptimizedFullHash { + static size_t hash(const TextAttributes& attributes) { + return std::hash{}(attributes); + } +}; + +struct LegacyLayoutHash { + static size_t hash(const TextAttributes& attributes) { + return legacyLayoutHash(attributes); + } +}; + +struct OptimizedLayoutHash { + static size_t hash(const TextAttributes& attributes) { + return textAttributesHashLayoutWise(attributes); + } +}; + +template +void textAttributesHashBenchmark(benchmark::State& state) { + const auto attributes = Attributes::make(); + for (auto _ : state) { + benchmark::DoNotOptimize(attributes); + benchmark::DoNotOptimize(Hash::hash(attributes)); + } +} + +BENCHMARK_TEMPLATE( + textAttributesHashBenchmark, + DefaultAttributes, + LegacyFullHash) + ->Name("LegacyFull_Default"); +BENCHMARK_TEMPLATE( + textAttributesHashBenchmark, + DefaultAttributes, + OptimizedFullHash) + ->Name("OptimizedFull_Default"); +BENCHMARK_TEMPLATE( + textAttributesHashBenchmark, + SparseAttributes, + LegacyFullHash) + ->Name("LegacyFull_Sparse"); +BENCHMARK_TEMPLATE( + textAttributesHashBenchmark, + SparseAttributes, + OptimizedFullHash) + ->Name("OptimizedFull_Sparse"); +BENCHMARK_TEMPLATE( + textAttributesHashBenchmark, + PartialAttributes, + LegacyFullHash) + ->Name("LegacyFull_Partial"); +BENCHMARK_TEMPLATE( + textAttributesHashBenchmark, + PartialAttributes, + OptimizedFullHash) + ->Name("OptimizedFull_Partial"); +BENCHMARK_TEMPLATE(textAttributesHashBenchmark, FullAttributes, LegacyFullHash) + ->Name("LegacyFull_Full"); +BENCHMARK_TEMPLATE( + textAttributesHashBenchmark, + FullAttributes, + OptimizedFullHash) + ->Name("OptimizedFull_Full"); + +BENCHMARK_TEMPLATE( + textAttributesHashBenchmark, + DefaultAttributes, + LegacyLayoutHash) + ->Name("LegacyLayout_Default"); +BENCHMARK_TEMPLATE( + textAttributesHashBenchmark, + DefaultAttributes, + OptimizedLayoutHash) + ->Name("OptimizedLayout_Default"); +BENCHMARK_TEMPLATE( + textAttributesHashBenchmark, + SparseAttributes, + LegacyLayoutHash) + ->Name("LegacyLayout_Sparse"); +BENCHMARK_TEMPLATE( + textAttributesHashBenchmark, + SparseAttributes, + OptimizedLayoutHash) + ->Name("OptimizedLayout_Sparse"); +BENCHMARK_TEMPLATE( + textAttributesHashBenchmark, + PartialAttributes, + LegacyLayoutHash) + ->Name("LegacyLayout_Partial"); +BENCHMARK_TEMPLATE( + textAttributesHashBenchmark, + PartialAttributes, + OptimizedLayoutHash) + ->Name("OptimizedLayout_Partial"); +BENCHMARK_TEMPLATE( + textAttributesHashBenchmark, + FullAttributes, + LegacyLayoutHash) + ->Name("LegacyLayout_Full"); +BENCHMARK_TEMPLATE( + textAttributesHashBenchmark, + FullAttributes, + OptimizedLayoutHash) + ->Name("OptimizedLayout_Full"); + +} // namespace +} // namespace facebook::react + +BENCHMARK_MAIN(); diff --git a/packages/react-native/ReactCommon/react/utils/hash_combine.h b/packages/react-native/ReactCommon/react/utils/hash_combine.h index e6ffd2983f7..668a1a023a6 100644 --- a/packages/react-native/ReactCommon/react/utils/hash_combine.h +++ b/packages/react-native/ReactCommon/react/utils/hash_combine.h @@ -7,7 +7,9 @@ #pragma once +#include #include +#include #include namespace facebook::react { @@ -32,4 +34,39 @@ std::size_t hash_combine(const T &v, const Args &...args) return seed; } +/* + * Combines a run of optional fields into `seed` as a single presence bitmask followed by the + * engaged values only. + * + * Each `hash_combine` step mixes into the previous seed, so it forms a dependency chain the CPU + * cannot overlap and an unset field still costs a full link. The optionals are hashed into an + * independent seed and merged into the preceding seed once, allowing both chains to overlap. + * Folding presence into one word keeps unused fields off the optional chain: a further optional + * costs a bit in the mask rather than a link. + * + * The mask is what keeps this collision-free. Skipping disengaged fields on its own would make the + * same value in two different slots hash identically. + * + * Both packs are expanded from the same parameter pack, so the mask's bit order cannot drift out + * of sync with the order the values are combined. + */ +template +void hash_combine_optionals(std::size_t &seed, const std::optional &...optionals) +{ + static_assert(sizeof...(Ts) <= 32, "presence mask holds 32 fields"); + + std::uint32_t presence = 0; + std::uint32_t bit = 1; + ((presence |= optionals.has_value() ? bit : 0u, bit <<= 1), ...); + std::size_t optionalsSeed = presence; + + auto combineIfEngaged = [&optionalsSeed](const auto &optional) { + if (optional.has_value()) { + hash_combine(optionalsSeed, *optional); + } + }; + (combineIfEngaged(optionals), ...); + hash_combine(seed, optionalsSeed); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/utils/tests/hash_combineTests.cpp b/packages/react-native/ReactCommon/react/utils/tests/hash_combineTests.cpp index 79bbde74994..9108c48c6f0 100644 --- a/packages/react-native/ReactCommon/react/utils/tests/hash_combineTests.cpp +++ b/packages/react-native/ReactCommon/react/utils/tests/hash_combineTests.cpp @@ -8,6 +8,12 @@ #include #include +#include +#include +#include +#include +#include + struct Person { std::string firstName; std::string lastName; @@ -79,4 +85,101 @@ TEST(hash_combineTests, testCustomTypes) { EXPECT_NE(hash_combine(person1), hash_combine(person2)); } +TEST(hash_combineTests, optionalsCombinePresenceBeforeEngagedValues) { + std::optional first{17}; + std::optional second; + std::optional third{true}; + + std::size_t actual = 41; + hash_combine_optionals(actual, first, second, third); + + std::size_t optionalsHash = std::uint32_t{0b101}; + hash_combine(optionalsHash, *first, *third); + std::size_t expected = 41; + hash_combine(expected, optionalsHash); + EXPECT_EQ(actual, expected); +} + +TEST(hash_combineTests, disengagedOptionalsOnlyCombinePresence) { + std::optional first; + std::optional second; + + std::size_t actual = 41; + hash_combine_optionals(actual, first, second); + + std::size_t expected = 41; + hash_combine(expected, std::uint32_t{0}); + EXPECT_EQ(actual, expected); +} + +TEST(hash_combineTests, optionalStringContentsAffectProvidedSeed) { + std::optional react{"react"}; + std::optional reactNative{"react native"}; + + std::size_t reactHash = 41; + hash_combine_optionals(reactHash, react); + + std::size_t optionalsHash = std::uint32_t{1}; + hash_combine(optionalsHash, *react); + std::size_t expected = 41; + hash_combine(expected, optionalsHash); + EXPECT_EQ(reactHash, expected); + + std::size_t reactNativeHash = 41; + hash_combine_optionals(reactNativeHash, reactNative); + EXPECT_NE(reactHash, reactNativeHash); +} + +TEST(hash_combineTests, emptyOptionalStringDiffersFromDisengaged) { + std::optional emptyString{""}; + std::optional disengaged; + + std::size_t emptyStringHash = 41; + hash_combine_optionals(emptyStringHash, emptyString); + + std::size_t disengagedHash = 41; + hash_combine_optionals(disengagedHash, disengaged); + + EXPECT_NE(emptyStringHash, disengagedHash); +} + +TEST(hash_combineTests, optionalPositionAffectsHash) { + std::optional engaged{17}; + std::optional disengaged; + + std::size_t firstPosition = 0; + hash_combine_optionals(firstPosition, engaged, disengaged); + + std::size_t secondPosition = 0; + hash_combine_optionals(secondPosition, disengaged, engaged); + + EXPECT_NE(firstPosition, secondPosition); +} + +namespace { + +template +void combineArrayOptionals( + std::size_t& seed, + const std::array, 32>& optionals, + std::index_sequence /*unused*/) { + hash_combine_optionals(seed, optionals[Indices]...); +} + +} // namespace + +TEST(hash_combineTests, supportsHighestPresenceBit) { + std::array, 32> optionals; + optionals.back() = 17; + + std::size_t actual = 41; + combineArrayOptionals(actual, optionals, std::make_index_sequence<32>{}); + + std::size_t optionalsHash = std::uint32_t{1} << 31; + hash_combine(optionalsHash, 17); + std::size_t expected = 41; + hash_combine(expected, optionalsHash); + EXPECT_EQ(actual, expected); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 1cda27553fb..d0f6a544583 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<0cdd169a97730ccd6c6fdb359f0eb30d>> + * @generated SignedSource<<5eb220592bfac74664903f2f697ff80c>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -747,8 +747,9 @@ declare type ____TextStyle_Internal = Readonly< declare type ____TextStyle_InternalBase = { readonly color?: ____ColorValue_Internal readonly fontFamily?: string + readonly fontFeatureSettings?: string readonly fontSize?: number - readonly fontStyle?: "italic" | "normal" + readonly fontStyle?: "italic" | "normal" | "oblique" | string readonly fontVariant?: ____FontVariantArray_Internal | string readonly fontVariationSettings?: ____FontVariationSettings_Internal readonly fontWeight?: ____FontWeight_Internal @@ -5758,7 +5759,7 @@ export { AlertOptions, // 8a116d2a AlertType, // 5ab91217 AndroidKeyboardEvent, // e03becc8 - Animated, // e99db73c + Animated, // 2e3ec6cd AppConfig, // 35c0ca70 AppRegistry, // 5bc2bced AppState, // 12012be5 @@ -5981,7 +5982,7 @@ export { StatusBarProps, // c2a44d88 StatusBarStyle, // 78f53eea StyleProp, // fa0e9b4a - StyleSheet, // f7fe407a + StyleSheet, // 2b599fe8 SubmitBehavior, // c4ddf490 Switch, // cf0d6ce5 SwitchChangeEvent, // 899635b1 @@ -5991,9 +5992,9 @@ export { TVViewPropsIOS, // 330ce7b5 TargetedEvent, // 16e98910 TaskProvider, // 266dedf2 - Text, // 3ccd8020 + Text, // d32897b9 TextContentType, // 239b3ecc - TextInput, // 89af456b + TextInput, // 9afbaa03 TextInputAndroidProps, // 9ebbc103 TextInputBlurEvent, // b77af40e TextInputChangeEvent, // f55eef98 @@ -6003,13 +6004,13 @@ export { TextInputIOSProps, // fb3c9327 TextInputInstance, // 5a0c0e0d TextInputKeyPressEvent, // 546c5d07 - TextInputProps, // a93c2e69 + TextInputProps, // 5e6f57bb TextInputSelectionChangeEvent, // e58f2abc TextInputSubmitEditingEvent, // 6bcb2aa5 TextInstance, // 05463a96 TextLayoutEvent, // 3f54186f - TextProps, // 2e3336ca - TextStyle, // d7678842 + TextProps, // cb13b79b + TextStyle, // 97373c20 ToastAndroid, // 88a8969a TouchableHighlight, // edab1b07 TouchableHighlightInstance, // b510c0eb diff --git a/packages/react-native/types_DEPRECATED/Libraries/StyleSheet/StyleSheetTypes.d.ts b/packages/react-native/types_DEPRECATED/Libraries/StyleSheet/StyleSheetTypes.d.ts index 95a4acd756f..218a3274d36 100644 --- a/packages/react-native/types_DEPRECATED/Libraries/StyleSheet/StyleSheetTypes.d.ts +++ b/packages/react-native/types_DEPRECATED/Libraries/StyleSheet/StyleSheetTypes.d.ts @@ -569,7 +569,16 @@ export interface TextStyle extends TextStyleIOS, TextStyleAndroid, ViewStyle { color?: ColorValue | undefined; fontFamily?: string | undefined; fontSize?: number | undefined; - fontStyle?: 'normal' | 'italic' | undefined; + /** + * `oblique` slants upright glyphs; `oblique ` gives the slant an explicit CSS angle, + * for example `'oblique 20deg'`. An omitted angle is 14 degrees, the same slant this library + * has always used for synthetic italic. An explicit `'slnt'` in `fontVariationSettings` takes + * precedence over both. + * + * The `string` arm exists only to admit the `oblique ` form, which neither Flow nor the + * generated types can spell more precisely; prefer one of the listed keywords. + */ + fontStyle?: 'normal' | 'italic' | 'oblique' | string | undefined; /** * Specifies font weight. The values 'normal' and 'bold' are supported * for most fonts. Not all fonts have a variant for each of the numeric @@ -607,6 +616,12 @@ export interface TextStyle extends TextStyleIOS, TextStyleAndroid, ViewStyle { | 'heavy' | 'black' | undefined; + /** + * Specifies OpenType feature tags using CSS syntax. Combines with + * `fontVariant`; where both name the same tag, `fontFeatureSettings` wins. + * `normal` or an empty string contributes no features of its own. + */ + fontFeatureSettings?: string | undefined; /** * Specifies OpenType font variation axis values using CSS syntax or an * object keyed by four-character axis tags. An empty string or object resets diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt index 39c9d92d003..58b345a7b98 100644 --- a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt @@ -120,6 +120,11 @@ internal class RNTesterApplication : Application(), ReactApplication { override fun onCreate() { ReactFontManager.getInstance().addCustomFont(this, "Rubik", R.font.rubik) ReactFontManager.getInstance().addCustomFont(this, "FiraCode", R.font.firacode) + // A font under `res/font` is only reachable by name once it is registered here; without this + // the family resolves to the default face and the example demonstrates nothing. The name is + // the family name inside the binary rather than the resource id, so the shared Text examples + // can pass one `fontFamily` string that resolves on this platform and on iOS. + ReactFontManager.getInstance().addCustomFont(this, "EB Garamond", R.font.ebgaramond_regular) super.onCreate() loadReactNative(this) } diff --git a/packages/rn-tester/android/app/src/main/res/font/ebgaramond_regular.ttf b/packages/rn-tester/android/app/src/main/res/font/ebgaramond_regular.ttf new file mode 100644 index 00000000000..f8c490b19d6 Binary files /dev/null and b/packages/rn-tester/android/app/src/main/res/font/ebgaramond_regular.ttf differ diff --git a/packages/rn-tester/js/examples/Text/TextExample.android.js b/packages/rn-tester/js/examples/Text/TextExample.android.js index a88bef0bac7..8189d95b351 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.android.js +++ b/packages/rn-tester/js/examples/Text/TextExample.android.js @@ -16,6 +16,7 @@ import hotdog from '../../assets/hotdog.jpg'; import RNTesterText from '../../components/RNTesterText'; import TextLegend from '../../components/TextLegend'; import TextAdjustsDynamicLayoutExample from './TextAdjustsDynamicLayoutExample'; +import TextFontFeatureSettingsExample from './TextFontFeatureSettingsExample'; import TextSharedExamples from './TextSharedExamples'; const TextInlineView = require('../../components/TextInlineView'); @@ -359,6 +360,9 @@ function IncludeFontPaddingExample(props: {}): React.Node { ); } +// Shared by every font-property example below: a small muted caption naming what the row sets, +// with the specimen underneath it, so the specimen carries only the glyphs being demonstrated. + function FontVariantsExample(props: {}): React.Node { return ( @@ -1704,6 +1708,7 @@ const examples = [ return ; }, }, + TextFontFeatureSettingsExample, { title: 'Font variants', name: 'fontVariants', diff --git a/packages/rn-tester/js/examples/Text/TextFontFeatureSettingsExample.js b/packages/rn-tester/js/examples/Text/TextFontFeatureSettingsExample.js new file mode 100644 index 00000000000..8b48db0c66f --- /dev/null +++ b/packages/rn-tester/js/examples/Text/TextFontFeatureSettingsExample.js @@ -0,0 +1,138 @@ +/** + * 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. + * + * @flow + * @format + */ + +import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; + +import RNTesterText from '../../components/RNTesterText'; +import * as React from 'react'; +import {TextInput, View} from 'react-native'; + +export const fontExampleStyles: { + row: {marginBottom: number}, + label: {fontSize: number, opacity: number}, +} = { + row: {marginBottom: 6}, + label: {fontSize: 11, opacity: 0.6}, +}; + +export function FontExampleRow(props: { + label: string, + children: React.Node, +}): React.Node { + return ( + + {props.label} + {props.children} + + ); +} + +// Shared by TextExample.ios.js and TextExample.android.js so the two platforms photograph the same +// rows in the same font. EB Garamond is bundled on iOS and registered under the same family name on +// Android, and it declares every tag these rows switch: 'smcp', 'onum', 'liga', 'dlig' and 'kern'. +// Neither platform's system faces can stand in: on iOS, Hoefler Text keeps its small caps in a +// separate face rather than a feature, so CoreText finds no 'smcp' to apply. +// +// There is no 'lnum' row on purpose. This font's default figures are already lining and its 'lnum' +// lookup covers neither them nor the oldstyle glyphs 'onum' produces, so the tag can never change +// anything here and the row would only look like a demonstration. +// +// The size is 24 rather than the 16 the other cards use. The 'liga' and 'dlig' rows turn on a +// ligature break that moves glyphs by about a pixel at 16pt: the ink genuinely changes, but nobody +// reading the screenshot can see it, which makes the row read as a no-op. Do not lower this. +const baseStyle = {fontFamily: 'EB Garamond', fontSize: 24}; + +const TextFontFeatureSettingsExample: RNTesterModuleExample = { + title: 'Font feature settings', + name: 'fontFeatureSettings', + render: function (): React.Node { + return ( + + + Waffle 0123 AV To + + + + Waffle + + + + + 0123456789 + + + + + Waffle st ct + + + + + Waffle fi fl + + + + + Waffle fi fl + + + + + AV To Wa + + + + + AV To Wa + + + + + Waffle 0123 + + + + + Waffle 0123 + + + + + Waffle 0123 + + + + + + + ); + }, +}; + +export default TextFontFeatureSettingsExample; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 2e6dc7e0b4e..c304d205cd2 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -519,6 +519,7 @@ static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_ALLOW_F static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_BACKGROUND_COLOR; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_BEST_WRITING_DIRECTION; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_FONT_FAMILY; +static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_FONT_FEATURE_SETTINGS; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_FONT_SIZE; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_FONT_SIZE_MULTIPLIER; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_FONT_STYLE; @@ -960,9 +961,11 @@ std::optional facebook::react::toRadians(const facebook: std::optional facebook::react::fontVariantFromCSSFontVariant(facebook::react::CSSFontVariant cssVariant); std::optional facebook::react::isolationFromString(std::string_view isolationSetting); std::optional facebook::react::fromCSSTransformFunction(const facebook::react::CSSTransformFunction& cssTransform); +std::optional facebook::react::resolveFontFeatureSettings(const facebook::react::TextAttributes& textAttributes, bool includeFontVariant = true); std::pair facebook::react::calculateAnimationProgress(uint64_t now, const facebook::react::LayoutAnimation& animation, const facebook::react::AnimationConfig& mutationConfig); std::string facebook::react::base64Encode(const std::string_view s); std::string facebook::react::componentNameByReactViewName(std::string viewName); +std::string facebook::react::fontVariantToOpenTypeFeatures(const facebook::react::FontVariant& fontVariant); std::string facebook::react::generateRandomUuidString(); std::string facebook::react::toString(const double& value); std::string facebook::react::toString(const facebook::react::AccessibilityRole& accessibilityRole); @@ -1059,6 +1062,8 @@ template void facebook::react::fromRawValue(const facebook::react::PropsParserContext&, const facebook::react::RawValue& rawValue, T& result); template std::shared_ptr facebook::react::getManagerByName(std::shared_ptr& contextContainer, const char name[]); +template +void facebook::react::hash_combine_optionals(std::size_t& seed, const std::optional &... optionals); uint8_t facebook::react::alphaFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::blueFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::greenFromColor(facebook::react::SharedColor color) noexcept; @@ -5082,6 +5087,7 @@ class facebook::react::TextAttributes : public facebook::react::DebugStringConve public std::optional textDecorationStyle; public std::optional textTransform; public std::optional baseWritingDirection; + public std::optional fontFeatureSettings; public std::optional fontVariationSettings; public std::string fontFamily; public std::vector textEffects; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 7ec351405ee..14d0c44f192 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -519,6 +519,7 @@ static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_ALLOW_F static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_BACKGROUND_COLOR; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_BEST_WRITING_DIRECTION; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_FONT_FAMILY; +static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_FONT_FEATURE_SETTINGS; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_FONT_SIZE; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_FONT_SIZE_MULTIPLIER; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_FONT_STYLE; @@ -958,9 +959,11 @@ std::optional facebook::react::toRadians(const facebook: std::optional facebook::react::fontVariantFromCSSFontVariant(facebook::react::CSSFontVariant cssVariant); std::optional facebook::react::isolationFromString(std::string_view isolationSetting); std::optional facebook::react::fromCSSTransformFunction(const facebook::react::CSSTransformFunction& cssTransform); +std::optional facebook::react::resolveFontFeatureSettings(const facebook::react::TextAttributes& textAttributes, bool includeFontVariant = true); std::pair facebook::react::calculateAnimationProgress(uint64_t now, const facebook::react::LayoutAnimation& animation, const facebook::react::AnimationConfig& mutationConfig); std::string facebook::react::base64Encode(const std::string_view s); std::string facebook::react::componentNameByReactViewName(std::string viewName); +std::string facebook::react::fontVariantToOpenTypeFeatures(const facebook::react::FontVariant& fontVariant); std::string facebook::react::generateRandomUuidString(); std::string facebook::react::toString(const double& value); std::string facebook::react::toString(const facebook::react::AccessibilityRole& accessibilityRole); @@ -1055,6 +1058,8 @@ template void facebook::react::fromRawValue(const facebook::react::PropsParserContext&, const facebook::react::RawValue& rawValue, T& result); template std::shared_ptr facebook::react::getManagerByName(std::shared_ptr& contextContainer, const char name[]); +template +void facebook::react::hash_combine_optionals(std::size_t& seed, const std::optional &... optionals); uint8_t facebook::react::alphaFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::blueFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::greenFromColor(facebook::react::SharedColor color) noexcept; @@ -4892,6 +4897,7 @@ class facebook::react::TextAttributes : public facebook::react::DebugStringConve public std::optional textDecorationStyle; public std::optional textTransform; public std::optional baseWritingDirection; + public std::optional fontFeatureSettings; public std::optional fontVariationSettings; public std::string fontFamily; public std::vector textEffects; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 6843410835c..c368090a95c 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -519,6 +519,7 @@ static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_ALLOW_F static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_BACKGROUND_COLOR; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_BEST_WRITING_DIRECTION; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_FONT_FAMILY; +static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_FONT_FEATURE_SETTINGS; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_FONT_SIZE; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_FONT_SIZE_MULTIPLIER; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_FONT_STYLE; @@ -960,9 +961,11 @@ std::optional facebook::react::toRadians(const facebook: std::optional facebook::react::fontVariantFromCSSFontVariant(facebook::react::CSSFontVariant cssVariant); std::optional facebook::react::isolationFromString(std::string_view isolationSetting); std::optional facebook::react::fromCSSTransformFunction(const facebook::react::CSSTransformFunction& cssTransform); +std::optional facebook::react::resolveFontFeatureSettings(const facebook::react::TextAttributes& textAttributes, bool includeFontVariant = true); std::pair facebook::react::calculateAnimationProgress(uint64_t now, const facebook::react::LayoutAnimation& animation, const facebook::react::AnimationConfig& mutationConfig); std::string facebook::react::base64Encode(const std::string_view s); std::string facebook::react::componentNameByReactViewName(std::string viewName); +std::string facebook::react::fontVariantToOpenTypeFeatures(const facebook::react::FontVariant& fontVariant); std::string facebook::react::generateRandomUuidString(); std::string facebook::react::toString(const double& value); std::string facebook::react::toString(const facebook::react::AccessibilityRole& accessibilityRole); @@ -1059,6 +1062,8 @@ template void facebook::react::fromRawValue(const facebook::react::PropsParserContext&, const facebook::react::RawValue& rawValue, T& result); template std::shared_ptr facebook::react::getManagerByName(std::shared_ptr& contextContainer, const char name[]); +template +void facebook::react::hash_combine_optionals(std::size_t& seed, const std::optional &... optionals); uint8_t facebook::react::alphaFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::blueFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::greenFromColor(facebook::react::SharedColor color) noexcept; @@ -5073,6 +5078,7 @@ class facebook::react::TextAttributes : public facebook::react::DebugStringConve public std::optional textDecorationStyle; public std::optional textTransform; public std::optional baseWritingDirection; + public std::optional fontFeatureSettings; public std::optional fontVariationSettings; public std::string fontFamily; public std::vector textEffects; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index d6dc3f80a6a..731da45a77a 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -3807,9 +3807,11 @@ std::optional facebook::react::toRadians(const facebook: std::optional facebook::react::fontVariantFromCSSFontVariant(facebook::react::CSSFontVariant cssVariant); std::optional facebook::react::isolationFromString(std::string_view isolationSetting); std::optional facebook::react::fromCSSTransformFunction(const facebook::react::CSSTransformFunction& cssTransform); +std::optional facebook::react::resolveFontFeatureSettings(const facebook::react::TextAttributes& textAttributes, bool includeFontVariant = true); std::pair facebook::react::calculateAnimationProgress(uint64_t now, const facebook::react::LayoutAnimation& animation, const facebook::react::AnimationConfig& mutationConfig); std::string facebook::react::base64Encode(const std::string_view s); std::string facebook::react::componentNameByReactViewName(std::string viewName); +std::string facebook::react::fontVariantToOpenTypeFeatures(const facebook::react::FontVariant& fontVariant); std::string facebook::react::generateRandomUuidString(); std::string facebook::react::toString(const double& value); std::string facebook::react::toString(const facebook::react::AccessibilityRole& accessibilityRole); @@ -3903,6 +3905,8 @@ template void facebook::react::fromRawValue(const facebook::react::PropsParserContext&, const facebook::react::RawValue& rawValue, T& result); template std::shared_ptr facebook::react::getManagerByName(std::shared_ptr& contextContainer, const char name[]); +template +void facebook::react::hash_combine_optionals(std::size_t& seed, const std::optional &... optionals); uint8_t facebook::react::alphaFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::blueFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::greenFromColor(facebook::react::SharedColor color) noexcept; @@ -7253,6 +7257,7 @@ class facebook::react::TextAttributes : public facebook::react::DebugStringConve public std::optional textDecorationStyle; public std::optional textTransform; public std::optional baseWritingDirection; + public std::optional fontFeatureSettings; public std::optional fontVariationSettings; public std::string fontFamily; public std::vector textEffects; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index da7a542fd69..68c5d9befff 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -3797,9 +3797,11 @@ std::optional facebook::react::toRadians(const facebook: std::optional facebook::react::fontVariantFromCSSFontVariant(facebook::react::CSSFontVariant cssVariant); std::optional facebook::react::isolationFromString(std::string_view isolationSetting); std::optional facebook::react::fromCSSTransformFunction(const facebook::react::CSSTransformFunction& cssTransform); +std::optional facebook::react::resolveFontFeatureSettings(const facebook::react::TextAttributes& textAttributes, bool includeFontVariant = true); std::pair facebook::react::calculateAnimationProgress(uint64_t now, const facebook::react::LayoutAnimation& animation, const facebook::react::AnimationConfig& mutationConfig); std::string facebook::react::base64Encode(const std::string_view s); std::string facebook::react::componentNameByReactViewName(std::string viewName); +std::string facebook::react::fontVariantToOpenTypeFeatures(const facebook::react::FontVariant& fontVariant); std::string facebook::react::generateRandomUuidString(); std::string facebook::react::toString(const double& value); std::string facebook::react::toString(const facebook::react::AccessibilityRole& accessibilityRole); @@ -3892,6 +3894,8 @@ template void facebook::react::fromRawValue(const facebook::react::PropsParserContext&, const facebook::react::RawValue& rawValue, T& result); template std::shared_ptr facebook::react::getManagerByName(std::shared_ptr& contextContainer, const char name[]); +template +void facebook::react::hash_combine_optionals(std::size_t& seed, const std::optional &... optionals); uint8_t facebook::react::alphaFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::blueFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::greenFromColor(facebook::react::SharedColor color) noexcept; @@ -7095,6 +7099,7 @@ class facebook::react::TextAttributes : public facebook::react::DebugStringConve public std::optional textDecorationStyle; public std::optional textTransform; public std::optional baseWritingDirection; + public std::optional fontFeatureSettings; public std::optional fontVariationSettings; public std::string fontFamily; public std::vector textEffects; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index a92742d8c14..9dc7d73566c 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -3807,9 +3807,11 @@ std::optional facebook::react::toRadians(const facebook: std::optional facebook::react::fontVariantFromCSSFontVariant(facebook::react::CSSFontVariant cssVariant); std::optional facebook::react::isolationFromString(std::string_view isolationSetting); std::optional facebook::react::fromCSSTransformFunction(const facebook::react::CSSTransformFunction& cssTransform); +std::optional facebook::react::resolveFontFeatureSettings(const facebook::react::TextAttributes& textAttributes, bool includeFontVariant = true); std::pair facebook::react::calculateAnimationProgress(uint64_t now, const facebook::react::LayoutAnimation& animation, const facebook::react::AnimationConfig& mutationConfig); std::string facebook::react::base64Encode(const std::string_view s); std::string facebook::react::componentNameByReactViewName(std::string viewName); +std::string facebook::react::fontVariantToOpenTypeFeatures(const facebook::react::FontVariant& fontVariant); std::string facebook::react::generateRandomUuidString(); std::string facebook::react::toString(const double& value); std::string facebook::react::toString(const facebook::react::AccessibilityRole& accessibilityRole); @@ -3903,6 +3905,8 @@ template void facebook::react::fromRawValue(const facebook::react::PropsParserContext&, const facebook::react::RawValue& rawValue, T& result); template std::shared_ptr facebook::react::getManagerByName(std::shared_ptr& contextContainer, const char name[]); +template +void facebook::react::hash_combine_optionals(std::size_t& seed, const std::optional &... optionals); uint8_t facebook::react::alphaFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::blueFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::greenFromColor(facebook::react::SharedColor color) noexcept; @@ -7244,6 +7248,7 @@ class facebook::react::TextAttributes : public facebook::react::DebugStringConve public std::optional textDecorationStyle; public std::optional textTransform; public std::optional baseWritingDirection; + public std::optional fontFeatureSettings; public std::optional fontVariationSettings; public std::string fontFamily; public std::vector textEffects; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 763c902b2e2..551f178180c 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -532,9 +532,11 @@ std::optional facebook::react::filterTypeFromString std::optional facebook::react::fontVariantFromCSSFontVariant(facebook::react::CSSFontVariant cssVariant); std::optional facebook::react::isolationFromString(std::string_view isolationSetting); std::optional facebook::react::fromCSSTransformFunction(const facebook::react::CSSTransformFunction& cssTransform); +std::optional facebook::react::resolveFontFeatureSettings(const facebook::react::TextAttributes& textAttributes, bool includeFontVariant = true); std::pair facebook::react::calculateAnimationProgress(uint64_t now, const facebook::react::LayoutAnimation& animation, const facebook::react::AnimationConfig& mutationConfig); std::string facebook::react::base64Encode(const std::string_view s); std::string facebook::react::componentNameByReactViewName(std::string viewName); +std::string facebook::react::fontVariantToOpenTypeFeatures(const facebook::react::FontVariant& fontVariant); std::string facebook::react::generateRandomUuidString(); std::string facebook::react::toString(const double& value); std::string facebook::react::toString(const facebook::react::AccessibilityRole& accessibilityRole); @@ -624,6 +626,8 @@ template void facebook::react::fromRawValue(const facebook::react::PropsParserContext&, const facebook::react::RawValue& rawValue, T& result); template std::shared_ptr facebook::react::getManagerByName(std::shared_ptr& contextContainer, const char name[]); +template +void facebook::react::hash_combine_optionals(std::size_t& seed, const std::optional &... optionals); uint8_t facebook::react::alphaFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::blueFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::greenFromColor(facebook::react::SharedColor color) noexcept; @@ -3574,6 +3578,7 @@ class facebook::react::TextAttributes : public facebook::react::DebugStringConve public std::optional textDecorationStyle; public std::optional textTransform; public std::optional baseWritingDirection; + public std::optional fontFeatureSettings; public std::optional fontVariationSettings; public std::string fontFamily; public std::vector textEffects; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index abad9815f5c..69dba95924c 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -530,9 +530,11 @@ std::optional facebook::react::filterTypeFromString std::optional facebook::react::fontVariantFromCSSFontVariant(facebook::react::CSSFontVariant cssVariant); std::optional facebook::react::isolationFromString(std::string_view isolationSetting); std::optional facebook::react::fromCSSTransformFunction(const facebook::react::CSSTransformFunction& cssTransform); +std::optional facebook::react::resolveFontFeatureSettings(const facebook::react::TextAttributes& textAttributes, bool includeFontVariant = true); std::pair facebook::react::calculateAnimationProgress(uint64_t now, const facebook::react::LayoutAnimation& animation, const facebook::react::AnimationConfig& mutationConfig); std::string facebook::react::base64Encode(const std::string_view s); std::string facebook::react::componentNameByReactViewName(std::string viewName); +std::string facebook::react::fontVariantToOpenTypeFeatures(const facebook::react::FontVariant& fontVariant); std::string facebook::react::generateRandomUuidString(); std::string facebook::react::toString(const double& value); std::string facebook::react::toString(const facebook::react::AccessibilityRole& accessibilityRole); @@ -621,6 +623,8 @@ template void facebook::react::fromRawValue(const facebook::react::PropsParserContext&, const facebook::react::RawValue& rawValue, T& result); template std::shared_ptr facebook::react::getManagerByName(std::shared_ptr& contextContainer, const char name[]); +template +void facebook::react::hash_combine_optionals(std::size_t& seed, const std::optional &... optionals); uint8_t facebook::react::alphaFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::blueFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::greenFromColor(facebook::react::SharedColor color) noexcept; @@ -3424,6 +3428,7 @@ class facebook::react::TextAttributes : public facebook::react::DebugStringConve public std::optional textDecorationStyle; public std::optional textTransform; public std::optional baseWritingDirection; + public std::optional fontFeatureSettings; public std::optional fontVariationSettings; public std::string fontFamily; public std::vector textEffects; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 0c58351b5c0..e7a71a36717 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -532,9 +532,11 @@ std::optional facebook::react::filterTypeFromString std::optional facebook::react::fontVariantFromCSSFontVariant(facebook::react::CSSFontVariant cssVariant); std::optional facebook::react::isolationFromString(std::string_view isolationSetting); std::optional facebook::react::fromCSSTransformFunction(const facebook::react::CSSTransformFunction& cssTransform); +std::optional facebook::react::resolveFontFeatureSettings(const facebook::react::TextAttributes& textAttributes, bool includeFontVariant = true); std::pair facebook::react::calculateAnimationProgress(uint64_t now, const facebook::react::LayoutAnimation& animation, const facebook::react::AnimationConfig& mutationConfig); std::string facebook::react::base64Encode(const std::string_view s); std::string facebook::react::componentNameByReactViewName(std::string viewName); +std::string facebook::react::fontVariantToOpenTypeFeatures(const facebook::react::FontVariant& fontVariant); std::string facebook::react::generateRandomUuidString(); std::string facebook::react::toString(const double& value); std::string facebook::react::toString(const facebook::react::AccessibilityRole& accessibilityRole); @@ -624,6 +626,8 @@ template void facebook::react::fromRawValue(const facebook::react::PropsParserContext&, const facebook::react::RawValue& rawValue, T& result); template std::shared_ptr facebook::react::getManagerByName(std::shared_ptr& contextContainer, const char name[]); +template +void facebook::react::hash_combine_optionals(std::size_t& seed, const std::optional &... optionals); uint8_t facebook::react::alphaFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::blueFromColor(facebook::react::SharedColor color) noexcept; uint8_t facebook::react::greenFromColor(facebook::react::SharedColor color) noexcept; @@ -3565,6 +3569,7 @@ class facebook::react::TextAttributes : public facebook::react::DebugStringConve public std::optional textDecorationStyle; public std::optional textTransform; public std::optional baseWritingDirection; + public std::optional fontFeatureSettings; public std::optional fontVariationSettings; public std::string fontFamily; public std::vector textEffects;