diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 871212392b5..589692cd3e2 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -6451,28 +6451,26 @@ public class com/facebook/react/views/text/TextAttributeProps { public fun isUnderlineTextDecorationSet ()Z } -public class com/facebook/react/views/text/TextAttributes { +public final class com/facebook/react/views/text/TextAttributes { public static final field DEFAULT_MAX_FONT_SIZE_MULTIPLIER F public fun ()V - public fun applyChild (Lcom/facebook/react/views/text/TextAttributes;)Lcom/facebook/react/views/text/TextAttributes; - public fun getAllowFontScaling ()Z - public fun getEffectiveFontSize ()I - public fun getEffectiveLetterSpacing ()F - public fun getEffectiveLineHeight ()F - public fun getEffectiveMaxFontSizeMultiplier ()F - public fun getFontSize ()F - public fun getHeightOfTallestInlineViewOrImage ()F - public fun getLetterSpacing ()F - public fun getLineHeight ()F - public fun getMaxFontSizeMultiplier ()F - public fun getTextTransform ()Lcom/facebook/react/views/text/TextTransform; - public fun setAllowFontScaling (Z)V - public fun setFontSize (F)V - public fun setHeightOfTallestInlineViewOrImage (F)V - public fun setLetterSpacing (F)V - public fun setLineHeight (F)V - public fun setMaxFontSizeMultiplier (F)V - public fun setTextTransform (Lcom/facebook/react/views/text/TextTransform;)V + public final fun applyChild (Lcom/facebook/react/views/text/TextAttributes;)Lcom/facebook/react/views/text/TextAttributes; + public final fun getAllowFontScaling ()Z + public final fun getEffectiveFontSize ()I + public final fun getEffectiveLetterSpacing ()F + public final fun getEffectiveLineHeight ()F + public final fun getEffectiveMaxFontSizeMultiplier ()F + public final fun getFontSize ()F + public final fun getHeightOfTallestInlineViewOrImage ()F + public final fun getLetterSpacing ()F + public final fun getLineHeight ()F + public final fun getMaxFontSizeMultiplier ()F + public final fun setAllowFontScaling (Z)V + public final fun setFontSize (F)V + public final fun setHeightOfTallestInlineViewOrImage (F)V + public final fun setLetterSpacing (F)V + public final fun setLineHeight (F)V + public final fun setMaxFontSizeMultiplier (F)V public fun toString ()Ljava/lang/String; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java index 689a54bd1ec..57ec0c078eb 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java @@ -109,7 +109,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { if (child instanceof ReactRawTextShadowNode) { String childText = ((ReactRawTextShadowNode) child).getText(); if (childText != null) { - sb.append(TextTransform.applyNonNull(childText, textAttributes.getTextTransform())); + sb.append(TextTransform.applyNonNull(childText, textAttributes.textTransform)); } } else if (child instanceof ReactBaseTextShadowNode) { buildSpannedFromShadowNode( @@ -264,8 +264,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { if (text != null) { // Handle text that is provided via a prop (e.g. the `value` and `defaultValue` props on // TextInput). - sb.append( - TextTransform.applyNonNull(text, textShadowNode.mTextAttributes.getTextTransform())); + sb.append(TextTransform.applyNonNull(text, textShadowNode.mTextAttributes.textTransform)); } buildSpannedFromShadowNode(textShadowNode, sb, ops, null, supportsInlineViews, inlineViews, 0); @@ -633,20 +632,21 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { @ReactProp(name = PROP_TEXT_TRANSFORM) public void setTextTransform(@Nullable String textTransform) { + TextTransform textTransformEnum = TextTransform.UNSET; if (textTransform == null) { - mTextAttributes.setTextTransform(TextTransform.UNSET); + textTransformEnum = TextTransform.UNSET; } else if ("none".equals(textTransform)) { - mTextAttributes.setTextTransform(TextTransform.NONE); + textTransformEnum = TextTransform.NONE; } else if ("uppercase".equals(textTransform)) { - mTextAttributes.setTextTransform(TextTransform.UPPERCASE); + textTransformEnum = TextTransform.UPPERCASE; } else if ("lowercase".equals(textTransform)) { - mTextAttributes.setTextTransform(TextTransform.LOWERCASE); + textTransformEnum = TextTransform.LOWERCASE; } else if ("capitalize".equals(textTransform)) { - mTextAttributes.setTextTransform(TextTransform.CAPITALIZE); + textTransformEnum = TextTransform.CAPITALIZE; } else { FLog.w(ReactConstants.TAG, "Invalid textTransform: " + textTransform); - mTextAttributes.setTextTransform(TextTransform.UNSET); } + mTextAttributes.textTransform = textTransformEnum; markUpdated(); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java deleted file mode 100644 index f1fbcfe9ac5..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * 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. - */ - -package com.facebook.react.views.text; - -import androidx.annotation.NonNull; -import com.facebook.common.logging.FLog; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.common.ReactConstants; -import com.facebook.react.uimanager.PixelUtil; -import com.facebook.react.uimanager.ViewDefaults; - -/* - * Currently, TextAttributes consists of a subset of text props that need to be passed from parent - * to child so inheritance can be implemented correctly. An example complexity that causes a prop - * to end up in TextAttributes is when multiple props need to be considered together to determine - * the rendered aka effective value. For example, to figure out the rendered/effective font size, - * you need to take into account the fontSize, maxFontSizeMultiplier, and allowFontScaling props. - */ -@Nullsafe(Nullsafe.Mode.LOCAL) -public class TextAttributes { - // Setting the default to 0 indicates that there is no max. - public static final float DEFAULT_MAX_FONT_SIZE_MULTIPLIER = 0.0f; - - private boolean mAllowFontScaling = true; - private float mFontSize = Float.NaN; - private float mLineHeight = Float.NaN; - private float mLetterSpacing = Float.NaN; - private float mMaxFontSizeMultiplier = Float.NaN; - private float mHeightOfTallestInlineViewOrImage = Float.NaN; - - @NonNull private TextTransform mTextTransform = TextTransform.UNSET; - - public TextAttributes() {} - - public TextAttributes applyChild(TextAttributes child) { - TextAttributes result = new TextAttributes(); - - // allowFontScaling is always determined by the root Text - // component so don't allow the child to overwrite it. - result.mAllowFontScaling = mAllowFontScaling; - - result.mFontSize = !Float.isNaN(child.mFontSize) ? child.mFontSize : mFontSize; - result.mLineHeight = !Float.isNaN(child.mLineHeight) ? child.mLineHeight : mLineHeight; - result.mLetterSpacing = - !Float.isNaN(child.mLetterSpacing) ? child.mLetterSpacing : mLetterSpacing; - result.mMaxFontSizeMultiplier = - !Float.isNaN(child.mMaxFontSizeMultiplier) - ? child.mMaxFontSizeMultiplier - : mMaxFontSizeMultiplier; - result.mHeightOfTallestInlineViewOrImage = - !Float.isNaN(child.mHeightOfTallestInlineViewOrImage) - ? child.mHeightOfTallestInlineViewOrImage - : mHeightOfTallestInlineViewOrImage; - result.mTextTransform = - child.mTextTransform != TextTransform.UNSET ? child.mTextTransform : mTextTransform; - - return result; - } - - // Getters and setters - // - - public boolean getAllowFontScaling() { - return mAllowFontScaling; - } - - public void setAllowFontScaling(boolean value) { - mAllowFontScaling = value; - } - - public float getFontSize() { - return mFontSize; - } - - public void setFontSize(float value) { - mFontSize = value; - } - - public float getLineHeight() { - return mLineHeight; - } - - public void setLineHeight(float value) { - mLineHeight = value; - } - - public float getLetterSpacing() { - return mLetterSpacing; - } - - public void setLetterSpacing(float value) { - mLetterSpacing = value; - } - - public float getMaxFontSizeMultiplier() { - return mMaxFontSizeMultiplier; - } - - public void setMaxFontSizeMultiplier(float maxFontSizeMultiplier) { - if (maxFontSizeMultiplier != 0 && maxFontSizeMultiplier < 1) { - FLog.w(ReactConstants.TAG, "maxFontSizeMultiplier must be NaN, 0, or >= 1"); - mMaxFontSizeMultiplier = Float.NaN; - return; - } - mMaxFontSizeMultiplier = maxFontSizeMultiplier; - } - - public float getHeightOfTallestInlineViewOrImage() { - return mHeightOfTallestInlineViewOrImage; - } - - public void setHeightOfTallestInlineViewOrImage(float value) { - mHeightOfTallestInlineViewOrImage = value; - } - - public TextTransform getTextTransform() { - return mTextTransform; - } - - public void setTextTransform(@NonNull TextTransform textTransform) { - mTextTransform = textTransform; - } - - // Getters for effective values - // - // In general, these return `Float.NaN` if the property doesn't have a value. - // - - // Always returns a value because uses a hardcoded default as a fallback. - public int getEffectiveFontSize() { - float fontSize = !Float.isNaN(mFontSize) ? mFontSize : ViewDefaults.FONT_SIZE_SP; - return mAllowFontScaling - ? (int) Math.ceil(PixelUtil.toPixelFromSP(fontSize, getEffectiveMaxFontSizeMultiplier())) - : (int) Math.ceil(PixelUtil.toPixelFromDIP(fontSize)); - } - - public float getEffectiveLineHeight() { - if (Float.isNaN(mLineHeight)) { - return Float.NaN; - } - - float lineHeight = - mAllowFontScaling - ? PixelUtil.toPixelFromSP(mLineHeight, getEffectiveMaxFontSizeMultiplier()) - : PixelUtil.toPixelFromDIP(mLineHeight); - - // Take into account the requested line height - // and the height of the inline images. - boolean useInlineViewHeight = - !Float.isNaN(mHeightOfTallestInlineViewOrImage) - && mHeightOfTallestInlineViewOrImage > lineHeight; - return useInlineViewHeight ? mHeightOfTallestInlineViewOrImage : lineHeight; - } - - public float getEffectiveLetterSpacing() { - if (Float.isNaN(mLetterSpacing)) { - return Float.NaN; - } - - float letterSpacingPixels = - mAllowFontScaling - ? PixelUtil.toPixelFromSP(mLetterSpacing, getEffectiveMaxFontSizeMultiplier()) - : PixelUtil.toPixelFromDIP(mLetterSpacing); - - // `letterSpacingPixels` and `getEffectiveFontSize` are both in pixels, - // yielding an accurate em value. - return letterSpacingPixels / getEffectiveFontSize(); - } - - // Never returns NaN - public float getEffectiveMaxFontSizeMultiplier() { - return !Float.isNaN(mMaxFontSizeMultiplier) - ? mMaxFontSizeMultiplier - : DEFAULT_MAX_FONT_SIZE_MULTIPLIER; - } - - public String toString() { - return ("TextAttributes {" - + "\n getAllowFontScaling(): " - + getAllowFontScaling() - + "\n getFontSize(): " - + getFontSize() - + "\n getEffectiveFontSize(): " - + getEffectiveFontSize() - + "\n getHeightOfTallestInlineViewOrImage(): " - + getHeightOfTallestInlineViewOrImage() - + "\n getLetterSpacing(): " - + getLetterSpacing() - + "\n getEffectiveLetterSpacing(): " - + getEffectiveLetterSpacing() - + "\n getLineHeight(): " - + getLineHeight() - + "\n getEffectiveLineHeight(): " - + getEffectiveLineHeight() - + "\n getTextTransform(): " - + getTextTransform() - + "\n getMaxFontSizeMultiplier(): " - + getMaxFontSizeMultiplier() - + "\n getEffectiveMaxFontSizeMultiplier(): " - + getEffectiveMaxFontSizeMultiplier() - + "\n}"); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.kt new file mode 100644 index 00000000000..c24523543a6 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributes.kt @@ -0,0 +1,137 @@ +/* + * 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. + */ + +package com.facebook.react.views.text + +import com.facebook.common.logging.FLog +import com.facebook.react.common.ReactConstants +import com.facebook.react.uimanager.PixelUtil +import com.facebook.react.uimanager.ViewDefaults + +/* + * Currently, TextAttributes consists of a subset of text props that need to be passed from parent + * to child so inheritance can be implemented correctly. An example complexity that causes a prop + * to end up in TextAttributes is when multiple props need to be considered together to determine + * the rendered aka effective value. For example, to figure out the rendered/effective font size, + * you need to take into account the fontSize, maxFontSizeMultiplier, and allowFontScaling props. + */ +public class TextAttributes { + public var allowFontScaling: Boolean = true + public var fontSize: Float = Float.NaN + public var lineHeight: Float = Float.NaN + public var letterSpacing: Float = Float.NaN + public var heightOfTallestInlineViewOrImage: Float = Float.NaN + + @JvmField internal var textTransform: TextTransform = TextTransform.UNSET + + public fun applyChild(child: TextAttributes): TextAttributes { + val result = TextAttributes() + + // allowFontScaling is always determined by the root Text + // component so don't allow the child to overwrite it. + result.allowFontScaling = allowFontScaling + + result.fontSize = if (!child.fontSize.isNaN()) child.fontSize else fontSize + result.lineHeight = if (!child.lineHeight.isNaN()) child.lineHeight else lineHeight + result.letterSpacing = if (!child.letterSpacing.isNaN()) child.letterSpacing else letterSpacing + result.maxFontSizeMultiplier = + if (!child.maxFontSizeMultiplier.isNaN()) child.maxFontSizeMultiplier + else maxFontSizeMultiplier + result.heightOfTallestInlineViewOrImage = + if (!child.heightOfTallestInlineViewOrImage.isNaN()) child.heightOfTallestInlineViewOrImage + else heightOfTallestInlineViewOrImage + result.textTransform = + if (child.textTransform != TextTransform.UNSET) child.textTransform else textTransform + + return result + } + + public var maxFontSizeMultiplier: Float = Float.NaN + set(maxFontSizeMultiplier) { + if (maxFontSizeMultiplier != 0f && + maxFontSizeMultiplier < 1 && + !maxFontSizeMultiplier.isNaN()) { + FLog.w(ReactConstants.TAG, "maxFontSizeMultiplier must be NaN, 0, or >= 1") + field = Float.NaN + return + } + field = maxFontSizeMultiplier + } + + public val effectiveFontSize: Int + get() { + val fontSize = if (!fontSize.isNaN()) fontSize else ViewDefaults.FONT_SIZE_SP + return if (allowFontScaling) { + Math.ceil(PixelUtil.toPixelFromSP(fontSize, effectiveMaxFontSizeMultiplier).toDouble()) + .toInt() + } else { + Math.ceil(PixelUtil.toPixelFromDIP(fontSize).toDouble()).toInt() + } + } + + public val effectiveLineHeight: Float + get() { + if (lineHeight.isNaN()) { + return Float.NaN + } + + val lineHeight: Float = + if (allowFontScaling) PixelUtil.toPixelFromSP(lineHeight, effectiveMaxFontSizeMultiplier) + else PixelUtil.toPixelFromDIP(lineHeight) + + // Take into account the requested line height + // and the height of the inline images. + val useInlineViewHeight = + !heightOfTallestInlineViewOrImage.isNaN() && heightOfTallestInlineViewOrImage > lineHeight + return if (useInlineViewHeight) heightOfTallestInlineViewOrImage else lineHeight + } + + public val effectiveLetterSpacing: Float + get() { + if (letterSpacing.isNaN()) { + return Float.NaN + } + + val letterSpacingPixels: Float = + if (allowFontScaling) + PixelUtil.toPixelFromSP(letterSpacing, effectiveMaxFontSizeMultiplier) + else PixelUtil.toPixelFromDIP(letterSpacing) + + // `letterSpacingPixels` and `getEffectiveFontSize` are both in pixels, + // yielding an accurate em value. + return letterSpacingPixels / effectiveFontSize + } + + public val effectiveMaxFontSizeMultiplier: Float + // Never returns NaN + get() = + if (!maxFontSizeMultiplier.isNaN()) maxFontSizeMultiplier + else DEFAULT_MAX_FONT_SIZE_MULTIPLIER + + override fun toString(): String = + """ + TextAttributes { + getAllowFontScaling(): $allowFontScaling + getFontSize(): $fontSize + getEffectiveFontSize(): $effectiveFontSize + getHeightOfTallestInlineViewOrImage(): $heightOfTallestInlineViewOrImage + getLetterSpacing(): $letterSpacing + getEffectiveLetterSpacing(): $effectiveLetterSpacing + getLineHeight(): $lineHeight + getEffectiveLineHeight(): $effectiveLineHeight + getTextTransform(): $textTransform + getMaxFontSizeMultiplier(): $maxFontSizeMultiplier + getEffectiveMaxFontSizeMultiplier(): $effectiveMaxFontSizeMultiplier + } + """ + .trimIndent() + + internal companion object { + // Setting the default to 0 indicates that there is no max. + public const val DEFAULT_MAX_FONT_SIZE_MULTIPLIER: Float = 0.0f + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.kt index 36b28d05f45..bd617c321bc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextTransform.kt @@ -19,13 +19,13 @@ internal enum class TextTransform { CAPITALIZE, UNSET; - companion object { + internal companion object { @JvmStatic fun apply(text: String?, textTransform: TextTransform?): String? = text?.applyTextTransform(textTransform) @JvmStatic - public fun applyNonNull(text: String, textTransform: TextTransform?): String = + fun applyNonNull(text: String, textTransform: TextTransform?): String = text.applyTextTransform(textTransform) } }