Migrate TextAttributes to Kotlin (#51448)

Summary:
Migrate com.facebook.react.views.text.TextAttributes to Kotlin.

`TextTransform` is exposed in the `textTransform` var setter, and there doesn't seem to be a clean way to avoid having to make the `TextTransform` class public again. I have limited its companion to keep it internal as much as possible.

## Changelog:

[INTERNAL] - Migrate com.facebook.react.views.text.TextAttributes to Kotlin

Pull Request resolved: https://github.com/facebook/react-native/pull/51448

Test Plan:
```bash
yarn test-android
yarn android
```

Reviewed By: cortinico

Differential Revision: D74978129

Pulled By: rshest

fbshipit-source-id: ea0594f01738b8c8f4696434fe76974bbb9ff661
This commit is contained in:
Mateo Guzmán
2025-05-21 03:25:11 -07:00
committed by Facebook GitHub Bot
parent dc737cb909
commit bcfa5ca474
5 changed files with 166 additions and 239 deletions
@@ -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 <init> ()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;
}
@@ -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();
}
@@ -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}");
}
}
@@ -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
}
}
@@ -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)
}
}