Compare commits

...
Author SHA1 Message Date
CodemodService BotandFacebook GitHub Bot 913c77d4cb Fix CQS signal readability-braces-around-statements in xplat/jsi/jsi (#53139)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/53139

Reviewed By: adamjernst

Differential Revision: D79623776
2025-08-07 12:58:12 -07:00
Nicola CortiandFacebook GitHub Bot 384677f58e Add tests for DisplayMetricsHolder (#52946)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/52946

This just add a set of unit tests for `DisplayMetricsHolder` as I'm working on this class recently.

Changelog:
[Internal] [Changed] -

Reviewed By: rshest, mdvacca

Differential Revision: D78981753

fbshipit-source-id: 5800d44d3131a58770a0049eb2d08306874b7183
2025-08-07 11:01:37 -07:00
Nicola CortiandFacebook GitHub Bot 2e76fc8e8e Correctly create the first modal state (#52835)
Summary:
There is currently a bug with Modals with New Architecture where the first frame is rendered incorrectly, specifically not accounting for all the vertical insets (only the status bar). This fixes it.

Specifically:
1. I've removed the caching of the statusbar height from `ReactModalHostView` as that was not working correctly. Sometimes the value returned `0` meaning that it was not yet computed when Fabric was asking for it. In the updated implementation we now query `FabricUIManager` given the `surfaceId` of the modal.
2. I've modified the logic to account for all the vertical insets, not just the status bar.

## Changelog:

[ANDROID] [FIXED] - Correctly account for insets on first render of Modals on New Arch

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

Test Plan:
Tested on Marketplace Location Picker and the picker is still working correctly:

 https://pxl.cl/7NjtJ

Reviewed By: mdvacca

Differential Revision: D78975126

Pulled By: cortinico

fbshipit-source-id: d7afb4fa5d2f43a7e33da3860432fa6dfe0dc8d7
2025-08-07 11:01:37 -07:00
Ruslan ShestopalyukandFacebook GitHub Bot 5fc23d7c62 Add type-safe API for passing around benchmark results (#53143)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53143

# Changelog:
[Internal] -

This refactors the way Fantom benchmark test results are passed to the top level, making it type safe and more maintainable.

Reviewed By: andrewdacenko

Differential Revision: D79812707

fbshipit-source-id: d8bfef7e1b0c11b277a08f5e4c810f8c1efd7f89
2025-08-07 10:46:26 -07:00
Nicola CortiandFacebook GitHub Bot e92da16a9b Migrate ClipboardModuleTest to use BridgelessReactContext (#53131)
Summary:
This test was still using the old `BridgeReactContext`, I'm migrating it to `BridgelessReactContext`.

## Changelog:

[INTERNAL] -

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

Test Plan: CI

Reviewed By: mdvacca

Differential Revision: D79801564

Pulled By: cortinico

fbshipit-source-id: 9bb96185505703a773597aeadfeeaeeb194532de
2025-08-07 10:42:26 -07:00
Ruslan LesiutinandFacebook GitHub Bot ae5df7ee35 Bump Electron to 37.2.4 (#53145)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/53145

# Changelog: [Internal]

See attached tasks.

Reviewed By: motiz88

Differential Revision: D79563948

fbshipit-source-id: a95b4e63d3a7d0d456c89ec7361e58fea0f5fb66
2025-08-07 10:40:57 -07:00
Sharif MahmoudandFacebook GitHub Bot dacd8f26fd Fix HEADER_SEARCH_PATHS for RuntimeExecutor when USE_FRAMEWORKS is enabled (#53099)
Summary:
`#include <ReactCommon/RuntimeExecutor.h>` stopped working in react-native 0.81 when using frameworks because it is not part of ReactCommon anymore when the split happened for iOS.

to fix this I am including RuntimeExecutor in search headers same way we include ReactCommon.

## Changelog:

[IOS] [FIXED] - Fix import RuntimeExecutor.h with USE_FRAMEWORKS

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

Test Plan:
You can enable USE_FRAMEWORKS and do `#include <react/renderer/uimanager/UIManager.h>` (which react-native-reanimated is doing).
Build will fail complaining that it can't find ReactCommon/RuntimeExecutor.h which is included in UIManager.h
Add my patch, it will work and build successfully

Reviewed By: cortinico

Differential Revision: D79796637

Pulled By: cipolleschi

fbshipit-source-id: f8bb669cfb9f4414653655ed98d2cc6bb431a3e5
2025-08-07 10:12:51 -07:00
Nicola CortiandFacebook GitHub Bot ede037ade7 Cleanup heightOfTallestInlineImage field (#52978)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/52978

This field is never written anywhere (neither in the internal codebase, nor in OSS).
I'm cleaning this us and simplifying the logic:
- Deprecating `effectiveLineHeight`
- Replacing all the usage of `effectiveLineHeight` with just `lineHeight`

Changelog:
[Android] [Changed] - Deprecate the field `TextAttributeProps.effectiveLineHeight`. This field was public but never used in OSS.

Reviewed By: mdvacca

Differential Revision: D79442393

fbshipit-source-id: c424a6def0257264cd160a2d7be48c2d0f47135e
2025-08-07 09:49:46 -07:00
Mateo GuzmánandFacebook GitHub Bot fa921b3c7b Migrate TextAttributeProps to Kotlin (#52452)
Summary:
Migrate com.facebook.react.views.text.TextAttributeProps to Kotlin.

## Changelog:

[Android][Changed] - Migrated TextAttributeProps to Kotlin. You might need to update your property access to use camelCase instead of Hungarian notation.

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

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

Reviewed By: rshest

Differential Revision: D79341238

Pulled By: cortinico

fbshipit-source-id: 455c7b48f47a0cf240aaf330e1fa3674798e7237
2025-08-07 09:49:46 -07:00
25 changed files with 1070 additions and 1075 deletions
+2 -2
View File
@@ -27,11 +27,11 @@
"license": "MIT",
"engines": {
"node": ">= 20.19.4",
"electron": ">=36.3.0"
"electron": ">=37.2.4"
},
"dependencies": {
"cross-spawn": "^7.0.6",
"electron": "36.3.0"
"electron": "37.2.4"
},
"devDependencies": {
"semver": "^7.1.3"
@@ -6213,97 +6213,74 @@ public final class com/facebook/react/views/text/ReactTypefaceUtils {
public static final fun parseFontWeight (Ljava/lang/String;)I
}
public class com/facebook/react/views/text/TextAttributeProps {
public static final field TA_KEY_ACCESSIBILITY_ROLE S
public static final field TA_KEY_ALIGNMENT S
public static final field TA_KEY_ALLOW_FONT_SCALING S
public static final field TA_KEY_BACKGROUND_COLOR S
public static final field TA_KEY_BEST_WRITING_DIRECTION S
public static final field TA_KEY_FONT_FAMILY S
public static final field TA_KEY_FONT_SIZE S
public static final field TA_KEY_FONT_SIZE_MULTIPLIER S
public static final field TA_KEY_FONT_STYLE S
public static final field TA_KEY_FONT_VARIANT S
public static final field TA_KEY_FONT_WEIGHT S
public static final field TA_KEY_FOREGROUND_COLOR S
public static final field TA_KEY_IS_HIGHLIGHTED S
public static final field TA_KEY_LAYOUT_DIRECTION S
public static final field TA_KEY_LETTER_SPACING S
public static final field TA_KEY_LINE_BREAK_STRATEGY S
public static final field TA_KEY_LINE_HEIGHT S
public static final field TA_KEY_MAX_FONT_SIZE_MULTIPLIER S
public static final field TA_KEY_OPACITY S
public static final field TA_KEY_ROLE S
public static final field TA_KEY_TEXT_DECORATION_COLOR S
public static final field TA_KEY_TEXT_DECORATION_LINE S
public static final field TA_KEY_TEXT_DECORATION_STYLE S
public static final field TA_KEY_TEXT_SHADOW_COLOR S
public static final field TA_KEY_TEXT_SHADOW_OFFSET_DX S
public static final field TA_KEY_TEXT_SHADOW_OFFSET_DY S
public static final field TA_KEY_TEXT_SHADOW_RADIUS S
public static final field TA_KEY_TEXT_TRANSFORM S
public final class com/facebook/react/views/text/TextAttributeProps {
public static final field Companion Lcom/facebook/react/views/text/TextAttributeProps$Companion;
public static final field TA_KEY_ACCESSIBILITY_ROLE I
public static final field TA_KEY_ALIGNMENT I
public static final field TA_KEY_ALLOW_FONT_SCALING I
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_SIZE I
public static final field TA_KEY_FONT_SIZE_MULTIPLIER I
public static final field TA_KEY_FONT_STYLE I
public static final field TA_KEY_FONT_VARIANT I
public static final field TA_KEY_FONT_WEIGHT I
public static final field TA_KEY_FOREGROUND_COLOR I
public static final field TA_KEY_IS_HIGHLIGHTED I
public static final field TA_KEY_LAYOUT_DIRECTION I
public static final field TA_KEY_LETTER_SPACING I
public static final field TA_KEY_LINE_BREAK_STRATEGY I
public static final field TA_KEY_LINE_HEIGHT I
public static final field TA_KEY_MAX_FONT_SIZE_MULTIPLIER I
public static final field TA_KEY_OPACITY I
public static final field TA_KEY_ROLE I
public static final field TA_KEY_TEXT_DECORATION_COLOR I
public static final field TA_KEY_TEXT_DECORATION_LINE I
public static final field TA_KEY_TEXT_DECORATION_STYLE I
public static final field TA_KEY_TEXT_SHADOW_COLOR I
public static final field TA_KEY_TEXT_SHADOW_OFFSET_DX I
public static final field TA_KEY_TEXT_SHADOW_OFFSET_DY I
public static final field TA_KEY_TEXT_SHADOW_RADIUS I
public static final field TA_KEY_TEXT_TRANSFORM I
public static final field UNSET I
protected field mAccessibilityRole Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole;
protected field mAllowFontScaling Z
protected field mBackgroundColor I
protected field mColor I
protected field mContainsImages Z
protected field mFontFamily Ljava/lang/String;
protected field mFontFeatureSettings Ljava/lang/String;
protected field mFontSize I
protected field mFontSizeInput F
protected field mFontStyle I
protected field mFontWeight I
protected field mHeightOfTallestInlineImage F
protected field mIncludeFontPadding Z
protected field mIsBackgroundColorSet Z
protected field mIsColorSet Z
protected field mIsLineThroughTextDecorationSet Z
protected field mIsUnderlineTextDecorationSet Z
protected field mLayoutDirection I
protected field mLetterSpacingInput F
protected field mLineHeight F
protected field mLineHeightInput F
protected field mMaxFontSizeMultiplier F
protected field mNumberOfLines I
protected field mOpacity F
protected field mRole Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role;
protected field mTextAlign I
protected field mTextShadowColor I
protected field mTextShadowOffsetDx F
protected field mTextShadowOffsetDy F
protected field mTextShadowRadius F
protected field mTextTransform Lcom/facebook/react/views/text/TextTransform;
public static fun fromMapBuffer (Lcom/facebook/react/common/mapbuffer/MapBuffer;)Lcom/facebook/react/views/text/TextAttributeProps;
public static fun fromReadableMap (Lcom/facebook/react/uimanager/ReactStylesDiffMap;)Lcom/facebook/react/views/text/TextAttributeProps;
public fun getAccessibilityRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole;
public fun getBackgroundColor ()I
public fun getColor ()I
public fun getEffectiveFontSize ()I
public fun getEffectiveLetterSpacing ()F
public fun getEffectiveLineHeight ()F
public static fun getEllipsizeMode (Ljava/lang/String;)Landroid/text/TextUtils$TruncateAt;
public fun getFontFamily ()Ljava/lang/String;
public fun getFontFeatureSettings ()Ljava/lang/String;
public fun getFontStyle ()I
public fun getFontWeight ()I
public static fun getHyphenationFrequency (Ljava/lang/String;)I
public static fun getJustificationMode (Lcom/facebook/react/uimanager/ReactStylesDiffMap;I)I
public static fun getLayoutDirection (Ljava/lang/String;)I
public fun getLetterSpacing ()F
public fun getOpacity ()F
public fun getRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role;
public static fun getTextAlignment (Lcom/facebook/react/uimanager/ReactStylesDiffMap;ZI)I
public static fun getTextBreakStrategy (Ljava/lang/String;)I
public fun getTextShadowColor ()I
public fun getTextShadowOffsetDx ()F
public fun getTextShadowOffsetDy ()F
public fun getTextShadowRadius ()F
public fun getTextTransform ()Lcom/facebook/react/views/text/TextTransform;
public fun isBackgroundColorSet ()Z
public fun isColorSet ()Z
public fun isLineThroughTextDecorationSet ()Z
public fun isUnderlineTextDecorationSet ()Z
public final fun getAccessibilityRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$AccessibilityRole;
public final fun getAllowFontScaling ()Z
public final fun getBackgroundColor ()Ljava/lang/Integer;
public final fun getColor ()Ljava/lang/Integer;
public final fun getEffectiveLetterSpacing ()F
public final fun getEffectiveLineHeight ()F
public final fun getFontFamily ()Ljava/lang/String;
public final fun getFontFeatureSettings ()Ljava/lang/String;
public final fun getFontSize ()I
public final fun getFontStyle ()I
public final fun getFontWeight ()I
public final fun getLayoutDirection ()I
public final fun getLetterSpacing ()F
public final fun getLineHeight ()F
public final fun getMaxFontSizeMultiplier ()F
public final fun getNumberOfLines ()I
public final fun getOpacity ()F
public final fun getRole ()Lcom/facebook/react/uimanager/ReactAccessibilityDelegate$Role;
public final fun getTextShadowColor ()I
public final fun getTextShadowOffsetDx ()F
public final fun getTextShadowOffsetDy ()F
public final fun getTextShadowRadius ()F
public final fun isBackgroundColorSet ()Z
public final fun isColorSet ()Z
public final fun isLineThroughTextDecorationSet ()Z
public final fun isUnderlineTextDecorationSet ()Z
}
public final class com/facebook/react/views/text/TextAttributeProps$Companion {
public final fun fromMapBuffer (Lcom/facebook/react/common/mapbuffer/MapBuffer;)Lcom/facebook/react/views/text/TextAttributeProps;
public final fun fromReadableMap (Lcom/facebook/react/uimanager/ReactStylesDiffMap;)Lcom/facebook/react/views/text/TextAttributeProps;
public final fun getEllipsizeMode (Ljava/lang/String;)Landroid/text/TextUtils$TruncateAt;
public final fun getHyphenationFrequency (Ljava/lang/String;)I
public final fun getJustificationMode (Lcom/facebook/react/uimanager/ReactStylesDiffMap;I)I
public final fun getLayoutDirection (Ljava/lang/String;)I
public final fun getTextAlignment (Lcom/facebook/react/uimanager/ReactStylesDiffMap;ZI)I
public final fun getTextBreakStrategy (Ljava/lang/String;)I
}
public final class com/facebook/react/views/text/TextAttributes {
@@ -70,6 +70,7 @@ import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatur
import com.facebook.react.internal.interop.InteropEventEmitter;
import com.facebook.react.modules.core.ReactChoreographer;
import com.facebook.react.modules.i18nmanager.I18nUtil;
import com.facebook.react.uimanager.DisplayMetricsHolder;
import com.facebook.react.uimanager.GuardedFrameCallback;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.PixelUtil;
@@ -97,6 +98,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -725,6 +727,23 @@ public class FabricUIManager
return true;
}
/**
* This method is used to get the encoded screen size without vertical insets for a given surface.
* It's used by the Modal component to determine the size of the screen without vertical insets.
* The method is private as it's accessed via JNI from C++.
*
* @param surfaceId The surface ID of the surface for which the Modal is going to render.
* @return The encoded screen size as a long (both width and height) are represented without
* vertical insets.
*/
private long getEncodedScreenSizeWithoutVerticalInsets(int surfaceId) {
SurfaceMountingManager surfaceMountingManager = mMountingManager.getSurfaceManager(surfaceId);
Objects.requireNonNull(surfaceMountingManager);
ThemedReactContext context = Objects.requireNonNull(surfaceMountingManager.getContext());
return DisplayMetricsHolder.getEncodedScreenSizeWithoutVerticalInsets(
context.getCurrentActivity());
}
@Override
public void addUIManagerEventListener(UIManagerListener listener) {
mListeners.add(listener);
@@ -16,6 +16,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.window.layout.WindowMetricsCalculator
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.WritableNativeMap
import com.facebook.react.uimanager.PixelUtil.pxToDp
import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn
/**
@@ -140,4 +141,33 @@ public object DisplayMetricsHolder {
WindowInsetsCompat.Type.displayCutout())
.top
}
/**
* Returns the encoded screen size without vertical insets.
*
* This is needed to render components that needs to be correctly positioned on the screen on
* their first frame. Modal is one of such components.
*
* @param activity the [Activity] to get the insets from.
* @return the encoded screen size as a [Long] value, where the first 32 bits represent the width
* and the last 32 bits represent the height in dp (density-independent pixels).
*/
// This annotation can be removed once FabricUIManager is migrated to Kotlin
@JvmName("getEncodedScreenSizeWithoutVerticalInsets")
@JvmStatic
internal fun getEncodedScreenSizeWithoutVerticalInsets(activity: Activity?): Long {
val windowInsets = activity?.window?.decorView?.let(ViewCompat::getRootWindowInsets) ?: return 0
val insets =
windowInsets.getInsets(
WindowInsetsCompat.Type.statusBars() or
WindowInsetsCompat.Type.navigationBars() or
WindowInsetsCompat.Type.displayCutout())
val verticalInsets = insets.top + insets.bottom
return encodeFloatsToLong(
(checkNotNull(screenDisplayMetrics).widthPixels).toFloat().pxToDp(),
(checkNotNull(screenDisplayMetrics).heightPixels - verticalInsets).toFloat().pxToDp())
}
internal fun encodeFloatsToLong(width: Float, height: Float): Long =
(width.toRawBits().toLong()) shl 32 or (height.toRawBits().toLong())
}
@@ -41,8 +41,6 @@ import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.common.annotations.VisibleForTesting
import com.facebook.react.common.build.ReactBuildConfig
import com.facebook.react.config.ReactFeatureFlags
import com.facebook.react.uimanager.DisplayMetricsHolder
import com.facebook.react.uimanager.DisplayMetricsHolder.getStatusBarHeightPx
import com.facebook.react.uimanager.JSPointerDispatcher
import com.facebook.react.uimanager.JSTouchDispatcher
import com.facebook.react.uimanager.PixelUtil.pxToDp
@@ -52,13 +50,11 @@ import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerModule
import com.facebook.react.uimanager.events.EventDispatcher
import com.facebook.react.views.common.ContextUtils
import com.facebook.react.views.modal.ReactModalHostView.DialogRootViewGroup
import com.facebook.react.views.view.ReactViewGroup
import com.facebook.react.views.view.disableEdgeToEdge
import com.facebook.react.views.view.enableEdgeToEdge
import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn
import com.facebook.react.views.view.setStatusBarTranslucency
import com.facebook.yoga.annotations.DoNotStrip
/**
* ReactModalHostView is a view that sits in the view hierarchy representing a Modal view.
@@ -73,7 +69,6 @@ import com.facebook.yoga.annotations.DoNotStrip
* addition and removal of views to the DialogRootViewGroup.
*/
@SuppressLint("ViewConstructor")
@DoNotStrip
public class ReactModalHostView(context: ThemedReactContext) :
ViewGroup(context), LifecycleEventListener {
@@ -132,7 +127,6 @@ public class ReactModalHostView(context: ThemedReactContext) :
private var createNewDialog = false
init {
initStatusBarHeight(context)
dialogRootViewGroup = DialogRootViewGroup(context)
}
@@ -485,26 +479,6 @@ public class ReactModalHostView(context: ThemedReactContext) :
private companion object {
private const val TAG = "ReactModalHost"
// We store the status bar height to be able to properly position
// the modal on the first render.
private var statusBarHeight = 0
private fun initStatusBarHeight(reactContext: ReactContext) {
statusBarHeight = getStatusBarHeightPx(reactContext.currentActivity)
}
@JvmStatic
@DoNotStrip
private fun getScreenDisplayMetricsWithoutInsets(): Long {
val displayMetrics = DisplayMetricsHolder.getScreenDisplayMetrics()
return encodeFloatsToLong(
displayMetrics.widthPixels.toFloat().pxToDp(),
(displayMetrics.heightPixels - statusBarHeight).toFloat().pxToDp())
}
private fun encodeFloatsToLong(width: Float, height: Float): Long =
(width.toRawBits().toLong()) shl 32 or (height.toRawBits().toLong())
}
/**
@@ -1,811 +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 android.os.Build;
import android.text.Layout;
import android.text.TextUtils;
import android.util.LayoutDirection;
import android.view.Gravity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.mapbuffer.MapBuffer;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactAccessibilityDelegate.AccessibilityRole;
import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.ViewProps;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
// TODO: T63643819 refactor naming of TextAttributeProps to make explicit that this represents
// TextAttributes and not TextProps. As part of this refactor extract methods that don't belong to
// TextAttributeProps (e.g. TextAlign)
@Nullsafe(Nullsafe.Mode.LOCAL)
public class TextAttributeProps {
// constants for Text Attributes serialization
public static final short TA_KEY_FOREGROUND_COLOR = 0;
public static final short TA_KEY_BACKGROUND_COLOR = 1;
public static final short TA_KEY_OPACITY = 2;
public static final short TA_KEY_FONT_FAMILY = 3;
public static final short TA_KEY_FONT_SIZE = 4;
public static final short TA_KEY_FONT_SIZE_MULTIPLIER = 5;
public static final short TA_KEY_FONT_WEIGHT = 6;
public static final short TA_KEY_FONT_STYLE = 7;
public static final short TA_KEY_FONT_VARIANT = 8;
public static final short TA_KEY_ALLOW_FONT_SCALING = 9;
public static final short TA_KEY_LETTER_SPACING = 10;
public static final short TA_KEY_LINE_HEIGHT = 11;
public static final short TA_KEY_ALIGNMENT = 12;
public static final short TA_KEY_BEST_WRITING_DIRECTION = 13;
public static final short TA_KEY_TEXT_DECORATION_COLOR = 14;
public static final short TA_KEY_TEXT_DECORATION_LINE = 15;
public static final short TA_KEY_TEXT_DECORATION_STYLE = 16;
public static final short TA_KEY_TEXT_SHADOW_RADIUS = 18;
public static final short TA_KEY_TEXT_SHADOW_COLOR = 19;
public static final short TA_KEY_TEXT_SHADOW_OFFSET_DX = 20;
public static final short TA_KEY_TEXT_SHADOW_OFFSET_DY = 21;
public static final short TA_KEY_IS_HIGHLIGHTED = 22;
public static final short TA_KEY_LAYOUT_DIRECTION = 23;
public static final short TA_KEY_ACCESSIBILITY_ROLE = 24;
public static final short TA_KEY_LINE_BREAK_STRATEGY = 25;
public static final short TA_KEY_ROLE = 26;
public static final short TA_KEY_TEXT_TRANSFORM = 27;
public static final short TA_KEY_MAX_FONT_SIZE_MULTIPLIER = 29;
public static final int UNSET = -1;
private static final String PROP_SHADOW_OFFSET = "textShadowOffset";
private static final String PROP_SHADOW_OFFSET_WIDTH = "width";
private static final String PROP_SHADOW_OFFSET_HEIGHT = "height";
private static final String PROP_SHADOW_RADIUS = "textShadowRadius";
private static final String PROP_SHADOW_COLOR = "textShadowColor";
private static final String PROP_TEXT_TRANSFORM = "textTransform";
private static final int DEFAULT_TEXT_SHADOW_COLOR = 0x55000000;
private static final int DEFAULT_JUSTIFICATION_MODE =
(Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? 0 : Layout.JUSTIFICATION_MODE_NONE;
private static final int DEFAULT_BREAK_STRATEGY = Layout.BREAK_STRATEGY_HIGH_QUALITY;
private static final int DEFAULT_HYPHENATION_FREQUENCY = Layout.HYPHENATION_FREQUENCY_NONE;
protected float mLineHeight = Float.NaN;
protected boolean mIsColorSet = false;
protected boolean mAllowFontScaling = true;
protected float mMaxFontSizeMultiplier = Float.NaN;
protected int mColor;
protected boolean mIsBackgroundColorSet = false;
protected int mBackgroundColor;
protected float mOpacity = Float.NaN;
protected int mNumberOfLines = ReactConstants.UNSET;
protected int mFontSize = ReactConstants.UNSET;
protected float mFontSizeInput = ReactConstants.UNSET;
protected float mLineHeightInput = ReactConstants.UNSET;
protected float mLetterSpacingInput = Float.NaN;
protected int mTextAlign = Gravity.NO_GRAVITY;
// `ReactConstants.UNSET` is -1, same as `LayoutDirection.UNDEFINED` (which is a hidden symbol)
protected int mLayoutDirection = ReactConstants.UNSET;
@NonNull protected TextTransform mTextTransform = TextTransform.NONE;
protected float mTextShadowOffsetDx = 0;
protected float mTextShadowOffsetDy = 0;
protected float mTextShadowRadius = 0;
protected int mTextShadowColor = DEFAULT_TEXT_SHADOW_COLOR;
protected boolean mIsUnderlineTextDecorationSet = false;
protected boolean mIsLineThroughTextDecorationSet = false;
protected boolean mIncludeFontPadding = true;
protected @Nullable AccessibilityRole mAccessibilityRole = null;
protected @Nullable Role mRole = null;
protected int mFontStyle = ReactConstants.UNSET;
protected int mFontWeight = ReactConstants.UNSET;
/**
* NB: If a font family is used that does not have a style in a certain Android version (ie.
* monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text
* nodes. To retain that style, you have to add it to those nodes explicitly.
*
* <p>Example, Android 4.4:
*
* <pre>
* <Text style={{fontFamily="serif" fontWeight="bold"}}>Bold Text</Text>
* <Text style={{fontFamily="sans-serif"}}>Bold Text</Text>
* <Text style={{fontFamily="serif}}>Bold Text</Text>
*
* <Text style={{fontFamily="monospace" fontWeight="bold"}}>Not Bold Text</Text>
* <Text style={{fontFamily="sans-serif"}}>Not Bold Text</Text>
* <Text style={{fontFamily="serif}}>Not Bold Text</Text>
*
* <Text style={{fontFamily="monospace" fontWeight="bold"}}>Not Bold Text</Text>
* <Text style={{fontFamily="sans-serif" fontWeight="bold"}}>Bold Text</Text>
* <Text style={{fontFamily="serif}}>Bold Text</Text>
* </pre>
*/
protected @Nullable String mFontFamily = null;
/**
* @see android.graphics.Paint#setFontFeatureSettings
*/
protected @Nullable String mFontFeatureSettings = null;
protected boolean mContainsImages = false;
protected float mHeightOfTallestInlineImage = Float.NaN;
private TextAttributeProps() {}
/** Build a TextAttributeProps using data from the {@link MapBuffer} received as a parameter. */
public static TextAttributeProps fromMapBuffer(MapBuffer props) {
TextAttributeProps result = new TextAttributeProps();
// TODO T83483191: Review constants that are not being set!
Iterator<MapBuffer.Entry> iterator = props.iterator();
while (iterator.hasNext()) {
MapBuffer.Entry entry = iterator.next();
switch (entry.getKey()) {
case TA_KEY_FOREGROUND_COLOR:
result.setColor(entry.getIntValue());
break;
case TA_KEY_BACKGROUND_COLOR:
result.setBackgroundColor(entry.getIntValue());
break;
case TA_KEY_OPACITY:
result.setOpacity((float) entry.getDoubleValue());
break;
case TA_KEY_FONT_FAMILY:
result.setFontFamily(entry.getStringValue());
break;
case TA_KEY_FONT_SIZE:
result.setFontSize((float) entry.getDoubleValue());
break;
case TA_KEY_FONT_SIZE_MULTIPLIER:
break;
case TA_KEY_FONT_WEIGHT:
result.setFontWeight(entry.getStringValue());
break;
case TA_KEY_FONT_STYLE:
result.setFontStyle(entry.getStringValue());
break;
case TA_KEY_FONT_VARIANT:
result.setFontVariant(entry.getMapBufferValue());
break;
case TA_KEY_ALLOW_FONT_SCALING:
result.setAllowFontScaling(entry.getBooleanValue());
break;
case TA_KEY_LETTER_SPACING:
result.setLetterSpacing((float) entry.getDoubleValue());
break;
case TA_KEY_LINE_HEIGHT:
result.setLineHeight((float) entry.getDoubleValue());
break;
case TA_KEY_ALIGNMENT:
break;
case TA_KEY_BEST_WRITING_DIRECTION:
break;
case TA_KEY_TEXT_DECORATION_COLOR:
break;
case TA_KEY_TEXT_DECORATION_LINE:
result.setTextDecorationLine(entry.getStringValue());
break;
case TA_KEY_TEXT_DECORATION_STYLE:
break;
case TA_KEY_TEXT_SHADOW_RADIUS:
result.setTextShadowRadius((float) entry.getDoubleValue());
break;
case TA_KEY_TEXT_SHADOW_COLOR:
result.setTextShadowColor(entry.getIntValue());
break;
case TA_KEY_TEXT_SHADOW_OFFSET_DX:
result.setTextShadowOffsetDx((float) entry.getDoubleValue());
break;
case TA_KEY_TEXT_SHADOW_OFFSET_DY:
result.setTextShadowOffsetDy((float) entry.getDoubleValue());
break;
case TA_KEY_IS_HIGHLIGHTED:
break;
case TA_KEY_LAYOUT_DIRECTION:
result.setLayoutDirection(entry.getStringValue());
break;
case TA_KEY_ACCESSIBILITY_ROLE:
result.setAccessibilityRole(entry.getStringValue());
break;
case TA_KEY_ROLE:
result.setRole(Role.values()[entry.getIntValue()]);
break;
case TA_KEY_TEXT_TRANSFORM:
result.setTextTransform(entry.getStringValue());
break;
case TA_KEY_MAX_FONT_SIZE_MULTIPLIER:
result.setMaxFontSizeMultiplier((float) entry.getDoubleValue());
break;
}
}
// TODO T83483191: Review why the following props are not serialized:
// setNumberOfLines
// setColor
// setIncludeFontPadding
return result;
}
public static TextAttributeProps fromReadableMap(ReactStylesDiffMap props) {
TextAttributeProps result = new TextAttributeProps();
result.setNumberOfLines(getIntProp(props, ViewProps.NUMBER_OF_LINES, ReactConstants.UNSET));
result.setLineHeight(getFloatProp(props, ViewProps.LINE_HEIGHT, ReactConstants.UNSET));
result.setLetterSpacing(getFloatProp(props, ViewProps.LETTER_SPACING, Float.NaN));
result.setAllowFontScaling(getBooleanProp(props, ViewProps.ALLOW_FONT_SCALING, true));
result.setMaxFontSizeMultiplier(
getFloatProp(props, ViewProps.MAX_FONT_SIZE_MULTIPLIER, Float.NaN));
result.setFontSize(getFloatProp(props, ViewProps.FONT_SIZE, ReactConstants.UNSET));
result.setColor(props.hasKey(ViewProps.COLOR) ? props.getInt(ViewProps.COLOR, 0) : null);
result.setColor(
props.hasKey(ViewProps.FOREGROUND_COLOR)
? props.getInt(ViewProps.FOREGROUND_COLOR, 0)
: null);
result.setBackgroundColor(
props.hasKey(ViewProps.BACKGROUND_COLOR)
? props.getInt(ViewProps.BACKGROUND_COLOR, 0)
: null);
result.setOpacity(getFloatProp(props, ViewProps.OPACITY, Float.NaN));
result.setFontFamily(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.setIncludeFontPadding(getBooleanProp(props, ViewProps.INCLUDE_FONT_PADDING, true));
result.setTextDecorationLine(getStringProp(props, ViewProps.TEXT_DECORATION_LINE));
result.setTextShadowOffset(
props.hasKey(PROP_SHADOW_OFFSET) ? props.getMap(PROP_SHADOW_OFFSET) : null);
result.setTextShadowRadius(getFloatProp(props, PROP_SHADOW_RADIUS, 1));
result.setTextShadowColor(getIntProp(props, PROP_SHADOW_COLOR, DEFAULT_TEXT_SHADOW_COLOR));
result.setTextTransform(getStringProp(props, PROP_TEXT_TRANSFORM));
result.setLayoutDirection(getStringProp(props, ViewProps.LAYOUT_DIRECTION));
result.setAccessibilityRole(getStringProp(props, ViewProps.ACCESSIBILITY_ROLE));
result.setRole(getStringProp(props, ViewProps.ROLE));
return result;
}
public static int getTextAlignment(ReactStylesDiffMap props, boolean isRTL, int defaultValue) {
if (!props.hasKey(ViewProps.TEXT_ALIGN)) {
return defaultValue;
}
String textAlignPropValue = props.getString(ViewProps.TEXT_ALIGN);
if ("justify".equals(textAlignPropValue)) {
return Gravity.LEFT;
} else {
if (textAlignPropValue == null || "auto".equals(textAlignPropValue)) {
return Gravity.NO_GRAVITY;
} else if ("left".equals(textAlignPropValue)) {
return isRTL ? Gravity.RIGHT : Gravity.LEFT;
} else if ("right".equals(textAlignPropValue)) {
return isRTL ? Gravity.LEFT : Gravity.RIGHT;
} else if ("center".equals(textAlignPropValue)) {
return Gravity.CENTER_HORIZONTAL;
} else {
FLog.w(ReactConstants.TAG, "Invalid textAlign: " + textAlignPropValue);
return Gravity.NO_GRAVITY;
}
}
}
public static int getJustificationMode(ReactStylesDiffMap props, int defaultValue) {
if (!props.hasKey(ViewProps.TEXT_ALIGN)) {
return defaultValue;
}
String textAlignPropValue = props.getString(ViewProps.TEXT_ALIGN);
if ("justify".equals(textAlignPropValue) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return Layout.JUSTIFICATION_MODE_INTER_WORD;
}
return DEFAULT_JUSTIFICATION_MODE;
}
private static boolean getBooleanProp(
ReactStylesDiffMap mProps, String name, boolean defaultValue) {
if (mProps.hasKey(name)) {
return mProps.getBoolean(name, defaultValue);
} else {
return defaultValue;
}
}
private static @Nullable String getStringProp(ReactStylesDiffMap mProps, String name) {
if (mProps.hasKey(name)) {
return mProps.getString(name);
} else {
return null;
}
}
private static int getIntProp(ReactStylesDiffMap mProps, String name, int defaultvalue) {
if (mProps.hasKey(name)) {
return mProps.getInt(name, defaultvalue);
} else {
return defaultvalue;
}
}
private static float getFloatProp(ReactStylesDiffMap mProps, String name, float defaultvalue) {
if (mProps.hasKey(name)) {
return mProps.getFloat(name, defaultvalue);
} else {
return defaultvalue;
}
}
private static @Nullable ReadableArray getArrayProp(ReactStylesDiffMap mProps, String name) {
if (mProps.hasKey(name)) {
return mProps.getArray(name);
} else {
return null;
}
}
// Returns a line height which takes into account the requested line height
// and the height of the inline images.
public float getEffectiveLineHeight() {
boolean useInlineViewHeight =
!Float.isNaN(mLineHeight)
&& !Float.isNaN(mHeightOfTallestInlineImage)
&& mHeightOfTallestInlineImage > mLineHeight;
return useInlineViewHeight ? mHeightOfTallestInlineImage : mLineHeight;
}
private void setNumberOfLines(int numberOfLines) {
mNumberOfLines = numberOfLines == 0 ? ReactConstants.UNSET : numberOfLines;
}
private void setLineHeight(float lineHeight) {
mLineHeightInput = lineHeight;
if (lineHeight == ReactConstants.UNSET) {
mLineHeight = Float.NaN;
} else {
mLineHeight =
mAllowFontScaling
? PixelUtil.toPixelFromSP(lineHeight)
: PixelUtil.toPixelFromDIP(lineHeight);
}
}
private void setLetterSpacing(float letterSpacing) {
mLetterSpacingInput = letterSpacing;
}
@NonNull
public TextTransform getTextTransform() {
return mTextTransform;
}
public float getLetterSpacing() {
float letterSpacingPixels =
mAllowFontScaling
? PixelUtil.toPixelFromSP(mLetterSpacingInput)
: PixelUtil.toPixelFromDIP(mLetterSpacingInput);
if (mFontSize <= 0) {
throw new IllegalArgumentException(
"FontSize should be a positive value. Current value: " + mFontSize);
}
// `letterSpacingPixels` and `mFontSize` are both in pixels,
// yielding an accurate em value.
return letterSpacingPixels / mFontSize;
}
public float getEffectiveLetterSpacing() {
return getLetterSpacing();
}
public int getEffectiveFontSize() {
return mFontSize;
}
private void setAllowFontScaling(boolean allowFontScaling) {
if (allowFontScaling != mAllowFontScaling) {
mAllowFontScaling = allowFontScaling;
setFontSize(mFontSizeInput);
setLineHeight(mLineHeightInput);
}
}
private void setMaxFontSizeMultiplier(float maxFontSizeMultiplier) {
if (maxFontSizeMultiplier != mMaxFontSizeMultiplier) {
mMaxFontSizeMultiplier = maxFontSizeMultiplier;
setFontSize(mFontSizeInput);
setLineHeight(mLineHeightInput);
}
}
private void setFontSize(float fontSize) {
mFontSizeInput = fontSize;
if (fontSize != ReactConstants.UNSET) {
fontSize =
mAllowFontScaling
? (float) Math.ceil(PixelUtil.toPixelFromSP(fontSize, mMaxFontSizeMultiplier))
: (float) Math.ceil(PixelUtil.toPixelFromDIP(fontSize));
}
mFontSize = (int) fontSize;
}
public int getColor() {
return mColor;
}
private void setColor(@Nullable Integer color) {
mIsColorSet = (color != null);
if (color != null) {
mColor = color;
}
}
public boolean isColorSet() {
return mIsColorSet;
}
public int getBackgroundColor() {
return mBackgroundColor;
}
private void setBackgroundColor(@Nullable Integer color) {
// TODO: Don't apply background color to anchor TextView since it will be applied on the View
// directly
// if (!isVirtualAnchor()) {
mIsBackgroundColorSet = (color != null);
if (color != null) {
mBackgroundColor = color;
}
// }
}
public float getOpacity() {
return mOpacity;
}
private void setOpacity(float opacity) {
mOpacity = opacity;
}
public boolean isBackgroundColorSet() {
return mIsBackgroundColorSet;
}
public int getFontStyle() {
return mFontStyle;
}
public @Nullable String getFontFamily() {
return mFontFamily;
}
private void setFontFamily(@Nullable String fontFamily) {
mFontFamily = fontFamily;
}
private void setFontVariant(@Nullable ReadableArray fontVariant) {
mFontFeatureSettings = ReactTypefaceUtils.parseFontVariant(fontVariant);
}
private void setFontVariant(@Nullable MapBuffer fontVariant) {
if (fontVariant == null || fontVariant.getCount() == 0) {
mFontFeatureSettings = null;
return;
}
List<String> features = new ArrayList<>();
Iterator<MapBuffer.Entry> iterator = fontVariant.iterator();
while (iterator.hasNext()) {
MapBuffer.Entry entry = iterator.next();
String value = entry.getStringValue();
if (value != null) {
switch (value) {
case "small-caps":
features.add("'smcp'");
break;
case "oldstyle-nums":
features.add("'onum'");
break;
case "lining-nums":
features.add("'lnum'");
break;
case "tabular-nums":
features.add("'tnum'");
break;
case "proportional-nums":
features.add("'pnum'");
break;
case "stylistic-one":
features.add("'ss01'");
break;
case "stylistic-two":
features.add("'ss02'");
break;
case "stylistic-three":
features.add("'ss03'");
break;
case "stylistic-four":
features.add("'ss04'");
break;
case "stylistic-five":
features.add("'ss05'");
break;
case "stylistic-six":
features.add("'ss06'");
break;
case "stylistic-seven":
features.add("'ss07'");
break;
case "stylistic-eight":
features.add("'ss08'");
break;
case "stylistic-nine":
features.add("'ss09'");
break;
case "stylistic-ten":
features.add("'ss10'");
break;
case "stylistic-eleven":
features.add("'ss11'");
break;
case "stylistic-twelve":
features.add("'ss12'");
break;
case "stylistic-thirteen":
features.add("'ss13'");
break;
case "stylistic-fourteen":
features.add("'ss14'");
break;
case "stylistic-fifteen":
features.add("'ss15'");
break;
case "stylistic-sixteen":
features.add("'ss16'");
break;
case "stylistic-seventeen":
features.add("'ss17'");
break;
case "stylistic-eighteen":
features.add("'ss18'");
break;
case "stylistic-nineteen":
features.add("'ss19'");
break;
case "stylistic-twenty":
features.add("'ss20'");
break;
}
}
}
mFontFeatureSettings = TextUtils.join(", ", features);
}
public @Nullable String getFontFeatureSettings() {
return mFontFeatureSettings;
}
public int getFontWeight() {
return mFontWeight;
}
private void setFontWeight(@Nullable String fontWeightString) {
mFontWeight = ReactTypefaceUtils.parseFontWeight(fontWeightString);
}
private void setFontStyle(@Nullable String fontStyleString) {
mFontStyle = ReactTypefaceUtils.parseFontStyle(fontStyleString);
}
private void setIncludeFontPadding(boolean includepad) {
mIncludeFontPadding = includepad;
}
private void setTextDecorationLine(@Nullable String textDecorationLineString) {
mIsUnderlineTextDecorationSet = false;
mIsLineThroughTextDecorationSet = false;
if (textDecorationLineString != null) {
for (String textDecorationLineSubString : textDecorationLineString.split("-")) {
if ("underline".equals(textDecorationLineSubString)) {
mIsUnderlineTextDecorationSet = true;
} else if ("strikethrough".equals(textDecorationLineSubString)) {
mIsLineThroughTextDecorationSet = true;
}
}
}
}
public boolean isUnderlineTextDecorationSet() {
return mIsUnderlineTextDecorationSet;
}
public boolean isLineThroughTextDecorationSet() {
return mIsLineThroughTextDecorationSet;
}
private void setTextShadowOffset(@Nullable ReadableMap offsetMap) {
mTextShadowOffsetDx = 0;
mTextShadowOffsetDy = 0;
if (offsetMap != null) {
if (offsetMap.hasKey(PROP_SHADOW_OFFSET_WIDTH)
&& !offsetMap.isNull(PROP_SHADOW_OFFSET_WIDTH)) {
mTextShadowOffsetDx =
PixelUtil.toPixelFromDIP(offsetMap.getDouble(PROP_SHADOW_OFFSET_WIDTH));
}
if (offsetMap.hasKey(PROP_SHADOW_OFFSET_HEIGHT)
&& !offsetMap.isNull(PROP_SHADOW_OFFSET_HEIGHT)) {
mTextShadowOffsetDy =
PixelUtil.toPixelFromDIP(offsetMap.getDouble(PROP_SHADOW_OFFSET_HEIGHT));
}
}
}
public float getTextShadowOffsetDx() {
return mTextShadowOffsetDx;
}
private void setTextShadowOffsetDx(float dx) {
mTextShadowOffsetDx = PixelUtil.toPixelFromDIP(dx);
}
public float getTextShadowOffsetDy() {
return mTextShadowOffsetDy;
}
private void setTextShadowOffsetDy(float dy) {
mTextShadowOffsetDy = PixelUtil.toPixelFromDIP(dy);
}
public static int getLayoutDirection(@Nullable String layoutDirection) {
int androidLayoutDirection;
if (layoutDirection == null || "undefined".equals(layoutDirection)) {
androidLayoutDirection = ReactConstants.UNSET;
} else if ("rtl".equals(layoutDirection)) {
androidLayoutDirection = LayoutDirection.RTL;
} else if ("ltr".equals(layoutDirection)) {
androidLayoutDirection = LayoutDirection.LTR;
} else {
FLog.w(ReactConstants.TAG, "Invalid layoutDirection: " + layoutDirection);
androidLayoutDirection = ReactConstants.UNSET;
}
return androidLayoutDirection;
}
private void setLayoutDirection(@Nullable String layoutDirection) {
mLayoutDirection = getLayoutDirection(layoutDirection);
}
public float getTextShadowRadius() {
return mTextShadowRadius;
}
private void setTextShadowRadius(float textShadowRadius) {
if (textShadowRadius != mTextShadowRadius) {
mTextShadowRadius = textShadowRadius;
}
}
public int getTextShadowColor() {
return mTextShadowColor;
}
private void setTextShadowColor(int textShadowColor) {
if (textShadowColor != mTextShadowColor) {
mTextShadowColor = textShadowColor;
}
}
private void setTextTransform(@Nullable String textTransform) {
if (textTransform == null || "none".equals(textTransform)) {
mTextTransform = TextTransform.NONE;
} else if ("uppercase".equals(textTransform)) {
mTextTransform = TextTransform.UPPERCASE;
} else if ("lowercase".equals(textTransform)) {
mTextTransform = TextTransform.LOWERCASE;
} else if ("capitalize".equals(textTransform)) {
mTextTransform = TextTransform.CAPITALIZE;
} else {
FLog.w(ReactConstants.TAG, "Invalid textTransform: " + textTransform);
mTextTransform = TextTransform.NONE;
}
}
public @Nullable AccessibilityRole getAccessibilityRole() {
return mAccessibilityRole;
}
private void setAccessibilityRole(@Nullable String accessibilityRole) {
if (accessibilityRole == null) {
mAccessibilityRole = null;
} else {
mAccessibilityRole = AccessibilityRole.fromValue(accessibilityRole);
}
}
@Nullable
public Role getRole() {
return mRole;
}
private void setRole(@Nullable String role) {
if (role == null) {
mRole = null;
} else {
mRole = Role.fromValue(role);
}
}
private void setRole(Role role) {
mRole = role;
}
public static int getTextBreakStrategy(@Nullable String textBreakStrategy) {
int androidTextBreakStrategy = DEFAULT_BREAK_STRATEGY;
if (textBreakStrategy != null) {
switch (textBreakStrategy) {
case "simple":
androidTextBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
break;
case "balanced":
androidTextBreakStrategy = Layout.BREAK_STRATEGY_BALANCED;
break;
default:
androidTextBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
break;
}
}
return androidTextBreakStrategy;
}
public static int getHyphenationFrequency(@Nullable String hyphenationFrequency) {
int androidHyphenationFrequency = DEFAULT_HYPHENATION_FREQUENCY;
if (hyphenationFrequency != null) {
switch (hyphenationFrequency) {
case "none":
androidHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
break;
case "normal":
androidHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NORMAL;
break;
default:
androidHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_FULL;
break;
}
}
return androidHyphenationFrequency;
}
public static @Nullable TextUtils.TruncateAt getEllipsizeMode(@Nullable String ellipsizeMode) {
@Nullable TextUtils.TruncateAt truncateAt = null;
if (ellipsizeMode != null) {
switch (ellipsizeMode) {
case "head":
truncateAt = TextUtils.TruncateAt.START;
break;
case "middle":
truncateAt = TextUtils.TruncateAt.MIDDLE;
break;
case "tail":
truncateAt = TextUtils.TruncateAt.END;
break;
case "clip":
truncateAt = null;
}
}
return truncateAt;
}
}
@@ -0,0 +1,561 @@
/*
* 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 android.os.Build
import android.text.Layout
import android.text.TextUtils
import android.text.TextUtils.TruncateAt
import android.util.LayoutDirection
import android.view.Gravity
import com.facebook.common.logging.FLog
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.ReactConstants
import com.facebook.react.common.mapbuffer.MapBuffer
import com.facebook.react.uimanager.PixelUtil.toPixelFromDIP
import com.facebook.react.uimanager.PixelUtil.toPixelFromSP
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.parseFontStyle
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontVariant
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
import kotlin.math.ceil
// TODO: T63643819 refactor naming of TextAttributeProps to make explicit that this represents
// TextAttributes and not TextProps. As part of this refactor extract methods that don't belong to
// TextAttributeProps (e.g. TextAlign)
public class TextAttributeProps private constructor() {
public var lineHeight: Float = Float.NaN
private set(value) {
lineHeightInput = value
field =
if (value == ReactConstants.UNSET.toFloat()) {
Float.NaN
} else {
if (allowFontScaling) toPixelFromSP(value) else toPixelFromDIP(value)
}
}
public var isColorSet: Boolean = false
private set
public var allowFontScaling: Boolean = true
private set(value) {
if (value != field) {
field = value
setFontSize(fontSizeInput)
lineHeight = lineHeightInput
}
}
public var maxFontSizeMultiplier: Float = Float.NaN
private set(value) {
if (value != field) {
field = value
setFontSize(fontSizeInput)
lineHeight = lineHeightInput
}
}
public var isBackgroundColorSet: Boolean = false
private set
public var opacity: Float = Float.NaN
private set
public var numberOfLines: Int = ReactConstants.UNSET
private set
public var fontSize: Int = ReactConstants.UNSET
private set
private var fontSizeInput: Float = ReactConstants.UNSET.toFloat()
private var lineHeightInput: Float = ReactConstants.UNSET.toFloat()
private var letterSpacingInput: Float = Float.NaN
// `ReactConstants.UNSET` is -1, same as `LayoutDirection.UNDEFINED` (which is a hidden symbol)
public var layoutDirection: Int = ReactConstants.UNSET
private set
internal var textTransform: TextTransform = TextTransform.NONE
public var isUnderlineTextDecorationSet: Boolean = false
private set
public var isLineThroughTextDecorationSet: Boolean = false
private set
private var includeFontPadding: Boolean = true
public var accessibilityRole: AccessibilityRole? = null
private set
public var role: ReactAccessibilityDelegate.Role? = null
private set
public var fontStyle: Int = ReactConstants.UNSET
private set
public var fontWeight: Int = ReactConstants.UNSET
private set
/**
* NB: If a font family is used that does not have a style in a certain Android version (ie.
* monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text
* nodes. To retain that style, you have to add it to those nodes explicitly.
*
* Example, Android 4.4:
* <pre>
* <Text style={{fontFamily="serif" fontWeight="bold" }}>Bold Text</Text>
* <Text style={{fontFamily="sans-serif"}}>Bold Text</Text>
* <Text style={{fontFamily="serif"}}>Bold Text</Text>
*
* <Text style={{fontFamily="monospace" fontWeight="bold" }}>Not Bold Text</Text>
* <Text style={{fontFamily="sans-serif"}}>Not Bold Text</Text>
* <Text style={{fontFamily="serif"}}>Not Bold Text</Text>
*
* <Text style={{fontFamily="monospace" fontWeight="bold" }}>Not Bold Text</Text>
* <Text style={{fontFamily="sans-serif" fontWeight="bold" }}>Bold Text</Text>
* <Text style={{fontFamily="serif"}}>Bold Text</Text>
* </pre> *
*/
public var fontFamily: String? = null
private set
/** @see android.graphics.Paint.setFontFeatureSettings */
public var fontFeatureSettings: String? = null
private set
@Deprecated("Use lineHeight instead", ReplaceWith("lineHeight"))
public val effectiveLineHeight: Float
get() = lineHeight
private fun setNumberOfLines(numberOfLines: Int) {
this.numberOfLines = if (numberOfLines == 0) ReactConstants.UNSET else numberOfLines
}
public var letterSpacing: Float
get() {
val letterSpacingPixels =
if (allowFontScaling) toPixelFromSP(letterSpacingInput)
else toPixelFromDIP(letterSpacingInput)
require(fontSize > 0) { "FontSize should be a positive value. Current value: $fontSize" }
// `letterSpacingPixels` and `fontSize` are both in pixels,
// yielding an accurate em value.
return letterSpacingPixels / fontSize
}
private set(letterSpacing) {
letterSpacingInput = letterSpacing
}
public val effectiveLetterSpacing: Float
get() = letterSpacing
private fun setFontSize(fontSize: Float) {
var fontSizeLocal = fontSize
fontSizeInput = fontSizeLocal
if (fontSizeLocal != ReactConstants.UNSET.toFloat()) {
fontSizeLocal =
if (allowFontScaling)
ceil(toPixelFromSP(fontSize, maxFontSizeMultiplier).toDouble()).toFloat()
else ceil(toPixelFromDIP(fontSize).toDouble()).toFloat()
}
this.fontSize = fontSizeLocal.toInt()
}
public var color: Int? = null
private set(value) {
isColorSet = (value != null)
if (value != null) {
field = value
}
}
public var backgroundColor: Int? = 0
private set(color) {
// TODO: Don't apply background color to anchor TextView since it will be applied on the
// View directly
// if (!isVirtualAnchor()) {
isBackgroundColorSet = (color != null)
if (color != null) {
field = color
}
// }
}
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<String?> = 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 = TextUtils.join(", ", features)
}
private fun setFontWeight(fontWeightString: String?) {
fontWeight = parseFontWeight(fontWeightString)
}
private fun setFontStyle(fontStyleString: String?) {
fontStyle = parseFontStyle(fontStyleString)
}
private fun setTextDecorationLine(textDecorationLineString: String?) {
isUnderlineTextDecorationSet = false
isLineThroughTextDecorationSet = false
if (textDecorationLineString != null) {
for (textDecorationLineSubString in
textDecorationLineString
.split("-".toRegex())
.dropLastWhile { it.isEmpty() }
.toTypedArray()) {
if ("underline" == textDecorationLineSubString) {
isUnderlineTextDecorationSet = true
} else if ("strikethrough" == textDecorationLineSubString) {
isLineThroughTextDecorationSet = true
}
}
}
}
private fun setTextShadowOffset(offsetMap: ReadableMap?) {
textShadowOffsetDx = 0f
textShadowOffsetDy = 0f
if (offsetMap != null) {
if (offsetMap.hasKey(PROP_SHADOW_OFFSET_WIDTH) &&
!offsetMap.isNull(PROP_SHADOW_OFFSET_WIDTH)) {
textShadowOffsetDx = toPixelFromDIP(offsetMap.getDouble(PROP_SHADOW_OFFSET_WIDTH))
}
if (offsetMap.hasKey(PROP_SHADOW_OFFSET_HEIGHT) &&
!offsetMap.isNull(PROP_SHADOW_OFFSET_HEIGHT)) {
textShadowOffsetDy = toPixelFromDIP(offsetMap.getDouble(PROP_SHADOW_OFFSET_HEIGHT))
}
}
}
public var textShadowOffsetDx: Float = 0f
private set(value) {
field = toPixelFromDIP(value)
}
public var textShadowOffsetDy: Float = 0f
private set(value) {
field = toPixelFromDIP(value)
}
private fun setLayoutDirection(layoutDirection: String?) {
this.layoutDirection = getLayoutDirection(layoutDirection)
}
public var textShadowRadius: Float = 0f
private set(value) {
if (value != field) {
field = value
}
}
public var textShadowColor: Int = DEFAULT_TEXT_SHADOW_COLOR
private set(value) {
if (value != field) {
field = value
}
}
private fun setTextTransform(textTransform: String?) {
this.textTransform =
when (textTransform) {
null,
"none" -> TextTransform.NONE
"uppercase" -> TextTransform.UPPERCASE
"lowercase" -> TextTransform.LOWERCASE
"capitalize" -> TextTransform.CAPITALIZE
else -> {
FLog.w(ReactConstants.TAG, "Invalid textTransform: $textTransform")
TextTransform.NONE
}
}
}
private fun setAccessibilityRole(accessibilityRole: String?) {
this.accessibilityRole =
if (accessibilityRole == null) null else AccessibilityRole.fromValue(accessibilityRole)
}
private fun setRole(role: String?) {
if (role == null) {
this.role = null
} else {
this.role = ReactAccessibilityDelegate.Role.fromValue(role)
}
}
private fun setRole(role: ReactAccessibilityDelegate.Role) {
this.role = role
}
public companion object {
// constants for Text Attributes serialization
public const val TA_KEY_FOREGROUND_COLOR: Int = 0
public const val TA_KEY_BACKGROUND_COLOR: Int = 1
public const val TA_KEY_OPACITY: Int = 2
public const val TA_KEY_FONT_FAMILY: Int = 3
public const val TA_KEY_FONT_SIZE: Int = 4
public const val TA_KEY_FONT_SIZE_MULTIPLIER: Int = 5
public const val TA_KEY_FONT_WEIGHT: Int = 6
public const val TA_KEY_FONT_STYLE: Int = 7
public const val TA_KEY_FONT_VARIANT: Int = 8
public const val TA_KEY_ALLOW_FONT_SCALING: Int = 9
public const val TA_KEY_LETTER_SPACING: Int = 10
public const val TA_KEY_LINE_HEIGHT: Int = 11
public const val TA_KEY_ALIGNMENT: Int = 12
public const val TA_KEY_BEST_WRITING_DIRECTION: Int = 13
public const val TA_KEY_TEXT_DECORATION_COLOR: Int = 14
public const val TA_KEY_TEXT_DECORATION_LINE: Int = 15
public const val TA_KEY_TEXT_DECORATION_STYLE: Int = 16
public const val TA_KEY_TEXT_SHADOW_RADIUS: Int = 18
public const val TA_KEY_TEXT_SHADOW_COLOR: Int = 19
public const val TA_KEY_TEXT_SHADOW_OFFSET_DX: Int = 20
public const val TA_KEY_TEXT_SHADOW_OFFSET_DY: Int = 21
public const val TA_KEY_IS_HIGHLIGHTED: Int = 22
public const val TA_KEY_LAYOUT_DIRECTION: Int = 23
public const val TA_KEY_ACCESSIBILITY_ROLE: Int = 24
public const val TA_KEY_LINE_BREAK_STRATEGY: Int = 25
public const val TA_KEY_ROLE: Int = 26
public const val TA_KEY_TEXT_TRANSFORM: Int = 27
public const val TA_KEY_MAX_FONT_SIZE_MULTIPLIER: Int = 29
public const val UNSET: Int = -1
private const val PROP_SHADOW_OFFSET = "textShadowOffset"
private const val PROP_SHADOW_OFFSET_WIDTH = "width"
private const val PROP_SHADOW_OFFSET_HEIGHT = "height"
private const val PROP_SHADOW_RADIUS = "textShadowRadius"
private const val PROP_SHADOW_COLOR = "textShadowColor"
private const val PROP_TEXT_TRANSFORM = "textTransform"
private const val DEFAULT_TEXT_SHADOW_COLOR = 0x55000000
private val DEFAULT_JUSTIFICATION_MODE =
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) 0 else Layout.JUSTIFICATION_MODE_NONE
private const val DEFAULT_BREAK_STRATEGY = Layout.BREAK_STRATEGY_HIGH_QUALITY
private const val DEFAULT_HYPHENATION_FREQUENCY = Layout.HYPHENATION_FREQUENCY_NONE
/** Build a TextAttributeProps using data from the [MapBuffer] received as a parameter. */
public fun fromMapBuffer(props: MapBuffer): TextAttributeProps {
val result = TextAttributeProps()
// TODO T83483191: Review constants that are not being set!
val iterator = props.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
when (entry.key) {
TA_KEY_FOREGROUND_COLOR -> result.color = entry.intValue
TA_KEY_BACKGROUND_COLOR -> result.backgroundColor = entry.intValue
TA_KEY_OPACITY -> result.opacity = entry.doubleValue.toFloat()
TA_KEY_FONT_FAMILY -> result.fontFamily = entry.stringValue
TA_KEY_FONT_SIZE -> result.setFontSize(entry.doubleValue.toFloat())
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)
TA_KEY_ALLOW_FONT_SCALING -> result.allowFontScaling = entry.booleanValue
TA_KEY_LETTER_SPACING -> result.letterSpacing = entry.doubleValue.toFloat()
TA_KEY_LINE_HEIGHT -> result.lineHeight = entry.doubleValue.toFloat()
TA_KEY_ALIGNMENT -> {}
TA_KEY_BEST_WRITING_DIRECTION -> {}
TA_KEY_TEXT_DECORATION_COLOR -> {}
TA_KEY_TEXT_DECORATION_LINE -> result.setTextDecorationLine(entry.stringValue)
TA_KEY_TEXT_DECORATION_STYLE -> {}
TA_KEY_TEXT_SHADOW_RADIUS -> result.textShadowRadius = entry.doubleValue.toFloat()
TA_KEY_TEXT_SHADOW_COLOR -> result.textShadowColor = entry.intValue
TA_KEY_TEXT_SHADOW_OFFSET_DX -> result.textShadowOffsetDx = entry.doubleValue.toFloat()
TA_KEY_TEXT_SHADOW_OFFSET_DY -> result.textShadowOffsetDy = entry.doubleValue.toFloat()
TA_KEY_IS_HIGHLIGHTED -> {}
TA_KEY_LAYOUT_DIRECTION -> result.setLayoutDirection(entry.stringValue)
TA_KEY_ACCESSIBILITY_ROLE -> result.setAccessibilityRole(entry.stringValue)
TA_KEY_ROLE -> result.setRole(ReactAccessibilityDelegate.Role.entries[entry.intValue])
TA_KEY_TEXT_TRANSFORM -> result.setTextTransform(entry.stringValue)
TA_KEY_MAX_FONT_SIZE_MULTIPLIER ->
result.maxFontSizeMultiplier = entry.doubleValue.toFloat()
}
}
// TODO T83483191: Review why the following props are not serialized:
// setNumberOfLines
// setColor
// setIncludeFontPadding
return result
}
public fun fromReadableMap(props: ReactStylesDiffMap): TextAttributeProps {
val result = TextAttributeProps()
result.setNumberOfLines(getIntProp(props, ViewProps.NUMBER_OF_LINES, ReactConstants.UNSET))
result.lineHeight = getFloatProp(props, ViewProps.LINE_HEIGHT, ReactConstants.UNSET.toFloat())
result.letterSpacing = getFloatProp(props, ViewProps.LETTER_SPACING, Float.NaN)
result.allowFontScaling = getBooleanProp(props, ViewProps.ALLOW_FONT_SCALING, true)
result.maxFontSizeMultiplier =
getFloatProp(props, ViewProps.MAX_FONT_SIZE_MULTIPLIER, Float.NaN)
result.setFontSize(getFloatProp(props, ViewProps.FONT_SIZE, ReactConstants.UNSET.toFloat()))
result.color = if (props.hasKey(ViewProps.COLOR)) props.getInt(ViewProps.COLOR, 0) else null
result.color =
if (props.hasKey(ViewProps.FOREGROUND_COLOR)) props.getInt(ViewProps.FOREGROUND_COLOR, 0)
else null
result.backgroundColor =
if (props.hasKey(ViewProps.BACKGROUND_COLOR)) props.getInt(ViewProps.BACKGROUND_COLOR, 0)
else null
result.opacity = getFloatProp(props, ViewProps.OPACITY, Float.NaN)
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.includeFontPadding = getBooleanProp(props, ViewProps.INCLUDE_FONT_PADDING, true)
result.setTextDecorationLine(getStringProp(props, ViewProps.TEXT_DECORATION_LINE))
result.setTextShadowOffset(
if (props.hasKey(PROP_SHADOW_OFFSET)) props.getMap(PROP_SHADOW_OFFSET) else null)
result.textShadowRadius = getFloatProp(props, PROP_SHADOW_RADIUS, 1f)
result.textShadowColor = getIntProp(props, PROP_SHADOW_COLOR, DEFAULT_TEXT_SHADOW_COLOR)
result.setTextTransform(getStringProp(props, PROP_TEXT_TRANSFORM))
result.setLayoutDirection(getStringProp(props, ViewProps.LAYOUT_DIRECTION))
result.setAccessibilityRole(getStringProp(props, ViewProps.ACCESSIBILITY_ROLE))
result.setRole(getStringProp(props, ViewProps.ROLE))
return result
}
public fun getTextAlignment(props: ReactStylesDiffMap, isRTL: Boolean, defaultValue: Int): Int {
if (!props.hasKey(ViewProps.TEXT_ALIGN)) {
return defaultValue
}
return when (val textAlignPropValue = props.getString(ViewProps.TEXT_ALIGN)) {
"justify" -> Gravity.LEFT
null,
"auto" -> Gravity.NO_GRAVITY
"left" -> if (isRTL) Gravity.RIGHT else Gravity.LEFT
"right" -> if (isRTL) Gravity.LEFT else Gravity.RIGHT
"center" -> Gravity.CENTER_HORIZONTAL
else -> {
FLog.w(ReactConstants.TAG, "Invalid textAlign: $textAlignPropValue")
Gravity.NO_GRAVITY
}
}
}
public fun getJustificationMode(props: ReactStylesDiffMap, defaultValue: Int): Int {
if (!props.hasKey(ViewProps.TEXT_ALIGN)) {
return defaultValue
}
val textAlignPropValue = props.getString(ViewProps.TEXT_ALIGN)
if ("justify" == textAlignPropValue && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return Layout.JUSTIFICATION_MODE_INTER_WORD
}
return DEFAULT_JUSTIFICATION_MODE
}
private fun getBooleanProp(
props: ReactStylesDiffMap,
name: String,
defaultValue: Boolean
): Boolean = if (props.hasKey(name)) props.getBoolean(name, defaultValue) else defaultValue
private fun getStringProp(props: ReactStylesDiffMap, name: String): String? =
if (props.hasKey(name)) props.getString(name) else null
private fun getIntProp(props: ReactStylesDiffMap, name: String, defaultValue: Int): Int =
if (props.hasKey(name)) props.getInt(name, defaultValue) else defaultValue
private fun getFloatProp(props: ReactStylesDiffMap, name: String, defaultValue: Float): Float =
if (props.hasKey(name)) props.getFloat(name, defaultValue) else defaultValue
private fun getArrayProp(props: ReactStylesDiffMap, name: String): ReadableArray? =
if (props.hasKey(name)) props.getArray(name) else null
public fun getLayoutDirection(layoutDirection: String?): Int {
return when (layoutDirection) {
null,
"undefined" -> ReactConstants.UNSET
"rtl" -> LayoutDirection.RTL
"ltr" -> LayoutDirection.LTR
else -> {
FLog.w(ReactConstants.TAG, "Invalid layoutDirection: $layoutDirection")
ReactConstants.UNSET
}
}
}
public fun getTextBreakStrategy(textBreakStrategy: String?): Int =
when (textBreakStrategy) {
null -> DEFAULT_BREAK_STRATEGY
"simple" -> Layout.BREAK_STRATEGY_SIMPLE
"balanced" -> Layout.BREAK_STRATEGY_BALANCED
else -> Layout.BREAK_STRATEGY_HIGH_QUALITY
}
public fun getHyphenationFrequency(hyphenationFrequency: String?): Int =
when (hyphenationFrequency) {
null -> DEFAULT_HYPHENATION_FREQUENCY
"none" -> Layout.HYPHENATION_FREQUENCY_NONE
"normal" -> Layout.HYPHENATION_FREQUENCY_NORMAL
else -> Layout.HYPHENATION_FREQUENCY_FULL
}
public fun getEllipsizeMode(ellipsizeMode: String?): TruncateAt? =
when (ellipsizeMode) {
"head" -> TruncateAt.START
"middle" -> TruncateAt.MIDDLE
"tail" -> TruncateAt.END
"clip" -> null
else -> null
}
}
}
@@ -231,7 +231,7 @@ internal object TextLayoutManager {
TextAttributeProps.fromMapBuffer(fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES))
sb.append(
TextTransform.apply(fragment.getString(FR_KEY_STRING), textAttributes.mTextTransform))
TextTransform.apply(fragment.getString(FR_KEY_STRING), textAttributes.textTransform))
val end = sb.length
val reactTag =
@@ -246,10 +246,10 @@ internal object TextLayoutManager {
TextInlineViewPlaceholderSpan(reactTag, width.toInt(), height.toInt())))
} else if (end >= start) {
val roleIsLink =
if (textAttributes.mRole != null)
(textAttributes.mRole == ReactAccessibilityDelegate.Role.LINK)
if (textAttributes.role != null)
(textAttributes.role == ReactAccessibilityDelegate.Role.LINK)
else
(textAttributes.mAccessibilityRole ==
(textAttributes.accessibilityRole ==
ReactAccessibilityDelegate.AccessibilityRole.LINK)
if (roleIsLink) {
if (ReactNativeFeatureFlags.enablePreparedTextLayout()) {
@@ -258,13 +258,17 @@ internal object TextLayoutManager {
ops.add(SetSpanOperation(start, end, ReactClickableSpan(reactTag)))
}
}
if (textAttributes.mIsColorSet) {
ops.add(SetSpanOperation(start, end, ReactForegroundColorSpan(textAttributes.mColor)))
if (textAttributes.isColorSet) {
textAttributes.color
?.let { ReactForegroundColorSpan(it) }
?.let { SetSpanOperation(start, end, it) }
?.let { ops.add(it) }
}
if (textAttributes.mIsBackgroundColorSet) {
ops.add(
SetSpanOperation(
start, end, ReactBackgroundColorSpan(textAttributes.mBackgroundColor)))
if (textAttributes.isBackgroundColorSet) {
textAttributes.backgroundColor
?.let { ReactBackgroundColorSpan(it) }
?.let { SetSpanOperation(start, end, it) }
?.let { ops.add(it) }
}
if (!textAttributes.opacity.isNaN()) {
ops.add(SetSpanOperation(start, end, ReactOpacitySpan(textAttributes.opacity)))
@@ -273,45 +277,43 @@ internal object TextLayoutManager {
ops.add(
SetSpanOperation(start, end, CustomLetterSpacingSpan(textAttributes.letterSpacing)))
}
ops.add(SetSpanOperation(start, end, ReactAbsoluteSizeSpan(textAttributes.mFontSize)))
if (textAttributes.mFontStyle != ReactConstants.UNSET ||
textAttributes.mFontWeight != ReactConstants.UNSET ||
textAttributes.mFontFamily != null) {
ops.add(SetSpanOperation(start, end, ReactAbsoluteSizeSpan(textAttributes.fontSize)))
if (textAttributes.fontStyle != ReactConstants.UNSET ||
textAttributes.fontWeight != ReactConstants.UNSET ||
textAttributes.fontFamily != null) {
ops.add(
SetSpanOperation(
start,
end,
CustomStyleSpan(
textAttributes.mFontStyle,
textAttributes.mFontWeight,
textAttributes.mFontFeatureSettings,
textAttributes.mFontFamily,
textAttributes.fontStyle,
textAttributes.fontWeight,
textAttributes.fontFeatureSettings,
textAttributes.fontFamily,
context.assets)))
}
if (textAttributes.mIsUnderlineTextDecorationSet) {
if (textAttributes.isUnderlineTextDecorationSet) {
ops.add(SetSpanOperation(start, end, ReactUnderlineSpan()))
}
if (textAttributes.mIsLineThroughTextDecorationSet) {
if (textAttributes.isLineThroughTextDecorationSet) {
ops.add(SetSpanOperation(start, end, ReactStrikethroughSpan()))
}
if ((textAttributes.mTextShadowOffsetDx != 0f ||
textAttributes.mTextShadowOffsetDy != 0f ||
textAttributes.mTextShadowRadius != 0f) &&
Color.alpha(textAttributes.mTextShadowColor) != 0) {
if ((textAttributes.textShadowOffsetDx != 0f ||
textAttributes.textShadowOffsetDy != 0f ||
textAttributes.textShadowRadius != 0f) &&
Color.alpha(textAttributes.textShadowColor) != 0) {
ops.add(
SetSpanOperation(
start,
end,
ShadowStyleSpan(
textAttributes.mTextShadowOffsetDx,
textAttributes.mTextShadowOffsetDy,
textAttributes.mTextShadowRadius,
textAttributes.mTextShadowColor)))
textAttributes.textShadowOffsetDx,
textAttributes.textShadowOffsetDy,
textAttributes.textShadowRadius,
textAttributes.textShadowColor)))
}
if (!textAttributes.effectiveLineHeight.isNaN()) {
ops.add(
SetSpanOperation(
start, end, CustomLineHeightSpan(textAttributes.effectiveLineHeight)))
if (!textAttributes.lineHeight.isNaN()) {
ops.add(SetSpanOperation(start, end, CustomLineHeightSpan(textAttributes.lineHeight)))
}
if (ReactNativeFeatureFlags.enablePreparedTextLayout()) {
@@ -409,12 +411,16 @@ internal object TextLayoutManager {
}
if (fragment.props.isColorSet) {
spannable.setSpan(ReactForegroundColorSpan(fragment.props.color), start, end, spanFlags)
spannable.setSpan(
fragment.props.color?.let { ReactForegroundColorSpan(it) }, start, end, spanFlags)
}
if (fragment.props.isBackgroundColorSet) {
spannable.setSpan(
ReactBackgroundColorSpan(fragment.props.backgroundColor), start, end, spanFlags)
fragment.props.backgroundColor?.let { ReactBackgroundColorSpan(it) },
start,
end,
spanFlags)
}
if (!fragment.props.opacity.isNaN()) {
@@ -426,8 +432,7 @@ internal object TextLayoutManager {
CustomLetterSpacingSpan(fragment.props.letterSpacing), start, end, spanFlags)
}
// TODO: Should this be using effectiveFontSize instead of fontSize?
spannable.setSpan(ReactAbsoluteSizeSpan(fragment.props.mFontSize), start, end, spanFlags)
spannable.setSpan(ReactAbsoluteSizeSpan(fragment.props.fontSize), start, end, spanFlags)
if (fragment.props.fontStyle != ReactConstants.UNSET ||
fragment.props.fontWeight != ReactConstants.UNSET ||
@@ -467,9 +472,8 @@ internal object TextLayoutManager {
spanFlags)
}
if (!fragment.props.effectiveLineHeight.isNaN()) {
spannable.setSpan(
CustomLineHeightSpan(fragment.props.effectiveLineHeight), start, end, spanFlags)
if (!fragment.props.lineHeight.isNaN()) {
spannable.setSpan(CustomLineHeightSpan(fragment.props.lineHeight), start, end, spanFlags)
}
if (ReactNativeFeatureFlags.enablePreparedTextLayout()) {
@@ -611,8 +615,8 @@ internal object TextLayoutManager {
baseTextAttributes: TextAttributeProps,
context: Context
) {
if (baseTextAttributes.effectiveFontSize != ReactConstants.UNSET) {
paint.textSize = baseTextAttributes.effectiveFontSize.toFloat()
if (baseTextAttributes.fontSize != ReactConstants.UNSET) {
paint.textSize = baseTextAttributes.fontSize.toFloat()
}
if (baseTextAttributes.fontStyle != ReactConstants.UNSET ||
@@ -11,12 +11,18 @@
package com.facebook.react.bridge
import android.app.Application
import com.facebook.react.bridge.queue.MessageQueueThreadSpec
import com.facebook.react.bridge.queue.ReactQueueConfiguration
import com.facebook.react.bridge.queue.ReactQueueConfigurationImpl
import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.runtime.BridgelessReactContext
import com.facebook.react.runtime.ReactHostImpl
import com.facebook.react.runtime.internal.bolts.Task
import com.facebook.react.uimanager.UIManagerModule
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.whenever
import org.robolectric.RuntimeEnvironment
@@ -49,4 +55,20 @@ object ReactTestHelper {
whenever(reactInstance.isDestroyed).thenReturn(false)
return reactInstance
}
@OptIn(UnstableReactNativeAPI::class)
fun createTestReactApplicationContext(application: Application): ReactApplicationContext {
val reactHost =
spy(
ReactHostImpl(
RuntimeEnvironment.getApplication(),
mock(),
mock(),
Task.Companion.IMMEDIATE_EXECUTOR,
Task.Companion.IMMEDIATE_EXECUTOR,
false /* allowPackagerServerAccess */,
false /* useDevSupport */,
))
return BridgelessReactContext(application, reactHost)
}
}
@@ -5,36 +5,45 @@
* LICENSE file in the root directory of this source tree.
*/
@file:Suppress("DEPRECATION")
package com.facebook.react.modules.clipboard
import android.annotation.SuppressLint
import android.content.ClipboardManager
import android.content.Context
import com.facebook.react.bridge.BridgeReactContext
import com.facebook.react.bridge.ReactTestHelper.createTestReactApplicationContext
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
import com.facebook.testutils.shadows.ShadowSoLoader
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@Suppress("DEPRECATION")
@SuppressLint("ClipboardManager", "DeprecatedClass")
@RunWith(RobolectricTestRunner::class)
@Config(shadows = [ShadowSoLoader::class])
class ClipboardModuleTest {
private lateinit var clipboardModule: ClipboardModule
private lateinit var clipboardManager: ClipboardManager
@Before
fun setUp() {
clipboardModule = ClipboardModule(BridgeReactContext(RuntimeEnvironment.getApplication()))
ReactNativeFeatureFlagsForTests.setUp()
clipboardModule =
ClipboardModule(createTestReactApplicationContext(RuntimeEnvironment.getApplication()))
clipboardManager =
RuntimeEnvironment.getApplication().getSystemService(Context.CLIPBOARD_SERVICE)
as ClipboardManager
}
@After
fun tearDown() {
ReactNativeFeatureFlags.dangerouslyReset()
}
@Suppress("DEPRECATION")
@Test
fun testSetString() {
clipboardModule.setString(TEST_CONTENT)
@@ -0,0 +1,174 @@
/*
* 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.uimanager
import android.annotation.TargetApi
import android.app.Activity
import android.content.Context
import android.util.DisplayMetrics
import android.view.View
import android.view.Window
import android.view.WindowInsets
import com.facebook.react.bridge.WritableMap
import com.facebook.testutils.shadows.ShadowNativeLoader
import com.facebook.testutils.shadows.ShadowNativeMap
import com.facebook.testutils.shadows.ShadowReadableNativeMap
import com.facebook.testutils.shadows.ShadowSoLoader
import com.facebook.testutils.shadows.ShadowWritableNativeMap
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(
shadows =
[
ShadowSoLoader::class,
ShadowNativeLoader::class,
ShadowNativeMap::class,
ShadowWritableNativeMap::class,
ShadowReadableNativeMap::class,
])
class DisplayMetricsHolderTest {
private lateinit var context: Context
private lateinit var displayMetrics: DisplayMetrics
@Before
fun setUp() {
context = RuntimeEnvironment.getApplication()
displayMetrics = context.resources.displayMetrics
DisplayMetricsHolder.setWindowDisplayMetrics(null)
DisplayMetricsHolder.setScreenDisplayMetrics(null)
}
@Test(expected = IllegalStateException::class)
fun getWindowDisplayMetrics_failsIfDisplayMetricsIsNotInitialized() {
DisplayMetricsHolder.getWindowDisplayMetrics()
}
@Test(expected = IllegalStateException::class)
fun getScreenDisplayMetrics_failsIfDisplayMetricsIsNotInitialized() {
DisplayMetricsHolder.getScreenDisplayMetrics()
}
@Test
fun setAndGetWindowDisplayMetrics_returnsSetValue() {
DisplayMetricsHolder.setWindowDisplayMetrics(displayMetrics)
val result = DisplayMetricsHolder.getWindowDisplayMetrics()
assertThat(result).isEqualTo(displayMetrics)
}
@Test
fun setAndGetScreenDisplayMetrics_returnsSetValue() {
DisplayMetricsHolder.setScreenDisplayMetrics(displayMetrics)
val result = DisplayMetricsHolder.getScreenDisplayMetrics()
assertThat(result).isEqualTo(displayMetrics)
}
@Test
fun initScreenDisplayMetrics_setsMetrics() {
DisplayMetricsHolder.initScreenDisplayMetrics(context)
assertThat(DisplayMetricsHolder.getScreenDisplayMetrics()).isNotNull()
}
@Test
fun initWindowDisplayMetrics_setsMetrics() {
DisplayMetricsHolder.initWindowDisplayMetrics(context)
assertThat(DisplayMetricsHolder.getWindowDisplayMetrics()).isNotNull()
}
@Test
fun initWindowMetricsIfNotInitialized_onlyInitializesOnce() {
DisplayMetricsHolder.initWindowDisplayMetrics(context)
val firstWindow = DisplayMetricsHolder.getWindowDisplayMetrics()
// Should not reinitialize
DisplayMetricsHolder.initWindowDisplayMetrics(context)
val secondWindow = DisplayMetricsHolder.getWindowDisplayMetrics()
assertThat(secondWindow).isEqualTo(firstWindow)
}
@Test
fun initScreenMetricsIfNotInitialized_onlyInitializesOnce() {
DisplayMetricsHolder.initScreenDisplayMetrics(context)
val firstScreen = DisplayMetricsHolder.getScreenDisplayMetrics()
// Should not reinitialize
DisplayMetricsHolder.initScreenDisplayMetrics(context)
val secondScreen = DisplayMetricsHolder.getScreenDisplayMetrics()
assertThat(secondScreen).isEqualTo(firstScreen)
}
@Test(expected = IllegalStateException::class)
fun getDisplayMetricsWritableMap_failsIfNotInitialized() {
DisplayMetricsHolder.getDisplayMetricsWritableMap(1.0)
}
@Test
fun getDisplayMetricsWritableMap_returnsCorrectMap() {
// Use the official initialization method to ensure both metrics are set
DisplayMetricsHolder.initWindowDisplayMetrics(context)
DisplayMetricsHolder.initScreenDisplayMetrics(context)
val map: WritableMap = DisplayMetricsHolder.getDisplayMetricsWritableMap(1.0)
assertThat(map.hasKey("windowPhysicalPixels")).isTrue()
assertThat(map.hasKey("screenPhysicalPixels")).isTrue()
val windowMap = map.getMap("windowPhysicalPixels")
val screenMap = map.getMap("screenPhysicalPixels")
checkNotNull(windowMap)
checkNotNull(screenMap)
assertThat(windowMap.hasKey("width")).isTrue()
assertThat(windowMap.hasKey("height")).isTrue()
assertThat(windowMap.hasKey("scale")).isTrue()
assertThat(windowMap.hasKey("fontScale")).isTrue()
assertThat(windowMap.hasKey("densityDpi")).isTrue()
}
@Test
@TargetApi(30)
fun getEncodedScreenSizeWithoutVerticalInsets_returnsEncodedValue() {
DisplayMetricsHolder.initScreenDisplayMetrics(context)
val activity: Activity = mock()
val window: Window = mock()
val decorView: View = mock()
val windowInsets: WindowInsets = mock()
whenever(activity.window).thenReturn(window)
whenever(window.decorView).thenReturn(decorView)
whenever(decorView.rootWindowInsets).thenReturn(windowInsets)
whenever(windowInsets.getInsets(anyInt()))
.thenReturn(android.graphics.Insets.of(10, 20, 10, 20))
// Should return a non-zero encoded value
val encoded = DisplayMetricsHolder.getEncodedScreenSizeWithoutVerticalInsets(activity)
assertThat(encoded).isNotZero()
}
@Test
fun getEncodedScreenSizeWithoutVerticalInsets_returnsZeroIfActivityIsNull() {
val encoded = DisplayMetricsHolder.getEncodedScreenSizeWithoutVerticalInsets(null)
assertThat(encoded).isZero()
}
@Test
fun encodeFloatsToLong_encodesWidthAndHeightCorrectly() {
val width = 123.45f
val height = 67.89f
val encoded = DisplayMetricsHolder.encodeFloatsToLong(width, height)
val decodedWidth = Float.fromBits((encoded shr 32).toInt())
val decodedHeight = Float.fromBits(encoded.toInt())
assertThat(decodedWidth).isEqualTo(width)
assertThat(decodedHeight).isEqualTo(height)
}
}
@@ -231,8 +231,9 @@ inline char hexDigit(unsigned x) {
// ASCII characters
bool isAllASCII(const char16_t* utf16, size_t length) {
for (const char16_t* e = utf16 + length; utf16 != e; ++utf16) {
if (*utf16 > 0x7F)
if (*utf16 > 0x7F) {
return false;
}
}
return true;
}
@@ -0,0 +1,48 @@
/*
* 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 "ModalHostViewComponentDescriptor.h"
namespace facebook::react {
#ifdef ANDROID
State::Shared ModalHostViewComponentDescriptor::createInitialState(
const Props::Shared& props,
const ShadowNodeFamily::Shared& family) const {
// For Android, we need to get the size of the screen without the vertical
// insets to correctly position the modal on the first rendering.
// For this reason we provide the `createInitialState` implementation
// that will query FabricUIManager for the size of the screen without
// vertical insets.
int surfaceId = family->getSurfaceId();
const jni::global_ref<jobject>& fabricUIManager =
contextContainer_->at<jni::global_ref<jobject>>("FabricUIManager");
static auto getEncodedScreenSizeWithoutVerticalInsets =
jni::findClassStatic(UIManagerJavaDescriptor)
->getMethod<jlong(jint)>("getEncodedScreenSizeWithoutVerticalInsets");
auto result =
getEncodedScreenSizeWithoutVerticalInsets(fabricUIManager, surfaceId);
// Inspired from yogaMeasureToSize from conversions.h
int32_t wBits = 0xFFFFFFFF & (result >> 32);
int32_t hBits = 0xFFFFFFFF & result;
auto* measuredWidth = reinterpret_cast<float*>(&wBits);
auto* measuredHeight = reinterpret_cast<float*>(&hBits);
return std::make_shared<ModalHostViewShadowNode::ConcreteState>(
std::make_shared<const ModalHostViewState>(ModalHostViewState(
Size{.width = *measuredWidth, .height = *measuredHeight})),
family);
}
#endif // ANDROID
} // namespace facebook::react
@@ -37,6 +37,16 @@ class ModalHostViewComponentDescriptor final
ConcreteComponentDescriptor::adopt(shadowNode);
}
#ifdef ANDROID
State::Shared createInitialState(
const Props::Shared& props,
const ShadowNodeFamily::Shared& family) const override;
#endif // ANDROID
private:
constexpr static auto UIManagerJavaDescriptor =
"com/facebook/react/fabric/FabricUIManager";
};
} // namespace facebook::react
@@ -1,38 +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.
*/
#pragma once
#include <fbjni/fbjni.h>
#include <react/renderer/graphics/Size.h>
namespace facebook::react {
class JReactModalHostView
: public facebook::jni::JavaClass<JReactModalHostView> {
public:
static auto constexpr kJavaDescriptor =
"Lcom/facebook/react/views/modal/ReactModalHostView;";
static Size getDisplayMetrics() {
static auto method =
JReactModalHostView::javaClassStatic()->getStaticMethod<jlong()>(
"getScreenDisplayMetricsWithoutInsets");
auto result = method(javaClassStatic());
// Inspired from yogaMeassureToSize from conversions.h
int32_t wBits = 0xFFFFFFFF & (result >> 32);
int32_t hBits = 0xFFFFFFFF & result;
auto* measuredWidth = reinterpret_cast<float*>(&wBits);
auto* measuredHeight = reinterpret_cast<float*>(&hBits);
return Size{.width = *measuredWidth, .height = *measuredHeight};
}
};
} // namespace facebook::react
@@ -7,12 +7,11 @@
#include <react/renderer/components/modal/ModalHostViewUtils.h>
#include <react/renderer/graphics/Size.h>
#include "JReactModalHostView.h"
namespace facebook::react {
Size ModalHostViewScreenSize() {
return JReactModalHostView::getDisplayMetrics();
return Size{0, 0};
}
} // namespace facebook::react
@@ -85,6 +85,7 @@ class NewArchitectureHelper
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-Fabric", "React_Fabric", ["react/renderer/components/view/platform/cxx"]))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-FabricImage", "React_FabricImage", []))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "ReactCommon", "ReactCommon", ["react/nativemodule/core"]))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-runtimeexecutor", "React_runtimeexecutor", ["platform/ios"]))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-NativeModulesApple", "React_NativeModulesApple", []))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-RCTFabric", "RCTFabric", []))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-utils", "React_utils", []))
@@ -313,6 +313,7 @@ class ReactNativePodsUtils
header_search_paths = config.build_settings["HEADER_SEARCH_PATHS"] ||= "$(inherited)"
ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "ReactCommon", "ReactCommon", ["react/nativemodule/core"])
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-runtimeexecutor", "React_runtimeexecutor", ["platform/ios"]))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "ReactCommon-Samples", "ReactCommon_Samples", ["platform/ios"]))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-Fabric", "React_Fabric", ["react/renderer/components/view/platform/cxx"], false))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-NativeModulesApple", "React_NativeModulesApple", []))
@@ -563,6 +564,7 @@ class ReactNativePodsUtils
def self.set_reactcommon_searchpaths(target_installation_result)
header_search_paths = ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "ReactCommon", "ReactCommon", ["react/nativemodule/core"])
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-runtimeexecutor", "React_runtimeexecutor", ["platform/ios"]))
.map { |search_path| "\"#{search_path}\"" }
ReactNativePodsUtils.update_header_paths_if_depends_on(target_installation_result, "ReactCommon", header_search_paths)
end
@@ -429,6 +429,7 @@ if use_frameworks
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks(\\"PODS_CONFIGURATION_BUILD_DIR\\", \\"React-FabricImage\\", \\"React_FabricImage\\", []))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks(\\"PODS_CONFIGURATION_BUILD_DIR\\", \\"React-graphics\\", \\"React_graphics\\", [\\"react/renderer/graphics/platform/ios\\"]))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks(\\"PODS_CONFIGURATION_BUILD_DIR\\", \\"ReactCommon\\", \\"ReactCommon\\", [\\"react/nativemodule/core\\"]))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks(\\"PODS_CONFIGURATION_BUILD_DIR\\", \\"React-runtimeexecutor\\", \\"React_runtimeexecutor\\", [\\"platform/ios\\"]))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks(\\"PODS_CONFIGURATION_BUILD_DIR\\", \\"React-NativeModulesApple\\", \\"React_NativeModulesApple\\", []))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks(\\"PODS_CONFIGURATION_BUILD_DIR\\", \\"React-RCTFabric\\", \\"RCTFabric\\", []))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks(\\"PODS_CONFIGURATION_BUILD_DIR\\", \\"React-debug\\", \\"React_debug\\", []))
@@ -907,6 +908,7 @@ if use_frameworks
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks(\\"PODS_CONFIGURATION_BUILD_DIR\\", \\"React-FabricImage\\", \\"React_FabricImage\\", []))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks(\\"PODS_CONFIGURATION_BUILD_DIR\\", \\"React-graphics\\", \\"React_graphics\\", [\\"react/renderer/graphics/platform/ios\\"]))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks(\\"PODS_CONFIGURATION_BUILD_DIR\\", \\"ReactCommon\\", \\"ReactCommon\\", [\\"react/nativemodule/core\\"]))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks(\\"PODS_CONFIGURATION_BUILD_DIR\\", \\"React-runtimeexecutor\\", \\"React_runtimeexecutor\\", [\\"platform/ios\\"]))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks(\\"PODS_CONFIGURATION_BUILD_DIR\\", \\"React-NativeModulesApple\\", \\"React_NativeModulesApple\\", []))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks(\\"PODS_CONFIGURATION_BUILD_DIR\\", \\"React-RCTFabric\\", \\"RCTFabric\\", []))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks(\\"PODS_CONFIGURATION_BUILD_DIR\\", \\"React-debug\\", \\"React_debug\\", []))
@@ -33,6 +33,7 @@ if use_frameworks
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-FabricImage", "React_FabricImage", []))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-graphics", "React_graphics", ["react/renderer/graphics/platform/ios"]))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "ReactCommon", "ReactCommon", ["react/nativemodule/core"]))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-runtimeexecutor", "React_runtimeexecutor", ["platform/ios"]))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-NativeModulesApple", "React_NativeModulesApple", []))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-RCTFabric", "RCTFabric", []))
.concat(ReactNativePodsUtils.create_header_search_path_for_frameworks("PODS_CONFIGURATION_BUILD_DIR", "React-debug", "React_debug", []))
+12 -28
View File
@@ -8,52 +8,36 @@
* @format
*/
import type {BenchmarkResult} from '../src/Benchmark';
import {markdownTable} from './utils';
type TestTaskTiming = {
name: string,
latency: {
mean: number,
min: number,
max: number,
p50: number,
p75: number,
p99: number,
},
};
export type BenchmarkTestArtifact = {
type: string,
timings: $ReadOnlyArray<TestTaskTiming>,
};
export const printBenchmarkResultsRanking = (
testResults: Array<{
benchmarkResults: Array<{
title: string,
testArtifact: mixed,
result: BenchmarkResult,
}>,
) => {
const testTaskTimings: {[string]: {[string]: number}} = {};
let numTestVariants = 0;
for (const testResult of testResults) {
// $FlowExpectedError[incompatible-cast]
const testArtifact = testResult?.testArtifact as ?BenchmarkTestArtifact;
for (const benchmarkResult of benchmarkResults) {
const result = benchmarkResult.result;
if (
testArtifact == null ||
testArtifact.timings == null ||
testArtifact.type !== 'benchmark' ||
testResult.title == null
result == null ||
result.timings == null ||
benchmarkResult.title == null
) {
continue;
}
numTestVariants++;
for (const taskTiming of testArtifact.timings) {
for (const taskTiming of result.timings) {
const taskName = taskTiming.name;
if (testTaskTimings[taskName] === undefined) {
testTaskTimings[taskName] = {};
}
testTaskTimings[taskName][testResult.title] = taskTiming.latency.p50;
testTaskTimings[taskName][benchmarkResult.title] =
taskTiming.latency?.p50 ?? taskTiming.latency.mean;
}
}
if (numTestVariants <= 1 || Object.keys(testTaskTimings).length === 0) {
+18 -19
View File
@@ -14,7 +14,7 @@ import type {
TestSuiteResult,
} from '../runtime/setup';
import type {TestSnapshotResults} from '../runtime/snapshotContext';
import type {BenchmarkTestArtifact} from './benchmarkUtils';
import type {BenchmarkResult} from '../src/Benchmark';
import type {
AsyncCommandResult,
ConsoleLogMessage,
@@ -80,7 +80,7 @@ function buildError(
async function processRNTesterCommandResult(
result: AsyncCommandResult,
): Promise<TestSuiteResult> {
): Promise<[TestSuiteResult, ?BenchmarkResult]> {
const stdoutChunks = [];
const stderrChunks = [];
@@ -92,7 +92,7 @@ async function processRNTesterCommandResult(
stderrChunks.push(chunk);
});
let testResult;
let testResult, benchmarkResult;
const rl = readline.createInterface({input: result.childProcess.stdout});
rl.on('line', (rawLine: string) => {
@@ -116,6 +116,9 @@ async function processRNTesterCommandResult(
case 'test-result':
testResult = parsed;
break;
case 'benchmark-result':
benchmarkResult = parsed;
break;
case 'console-log':
printConsoleLog(parsed);
break;
@@ -152,7 +155,7 @@ async function processRNTesterCommandResult(
);
}
return testResult;
return [testResult, benchmarkResult];
}
function generateBytecodeBundle({
@@ -223,6 +226,7 @@ module.exports = async function runTest(
);
const testResultsByConfig = [];
const benchmarkResults = [];
const skippedTestResults = ({
ancestorTitles,
@@ -241,7 +245,6 @@ module.exports = async function runTest(
snapshotResults: {} as TestSnapshotResults,
status: 'pending' as TestCaseResult['status'],
testFilePath: testPath,
testArtifact: {} as mixed,
title,
},
];
@@ -371,9 +374,8 @@ module.exports = async function runTest(
hermesVariant: testConfig.hermesVariant,
});
const processedResult = await processRNTesterCommandResult(
rnTesterCommandResult,
);
const [processedResult, benchmarkResult] =
await processRNTesterCommandResult(rnTesterCommandResult);
if (containsError(processedResult) || EnvironmentOptions.profileJS) {
await createSourceMap({
@@ -439,6 +441,13 @@ module.exports = async function runTest(
});
}
if (benchmarkResult != null) {
benchmarkResults.push({
title: testResults[0]?.ancestorTitles?.[0] ?? maybeCommonAncestor,
result: benchmarkResult,
});
}
testResultsByConfig.push(testResults);
}
@@ -455,17 +464,7 @@ module.exports = async function runTest(
snapshotResults,
);
printBenchmarkResultsRanking(
testResults.map(testResult => {
// $FlowExpectedError[incompatible-cast]
const testArtifact = testResult.testArtifact as ?BenchmarkTestArtifact;
const title = testResult.ancestorTitles[0];
return {
title,
testArtifact,
};
}),
);
printBenchmarkResultsRanking(benchmarkResults);
return {
testFilePath: testPath,
+9 -3
View File
@@ -8,6 +8,7 @@
* @format
*/
import type {BenchmarkResult} from '../src/Benchmark';
import type {SnapshotConfig, TestSnapshotResults} from './snapshotContext';
import {getConstants} from '../src/Constants';
@@ -26,7 +27,6 @@ export type TestCaseResult = {
failureDetails: Array<FailureDetail>,
numPassingAsserts: number,
snapshotResults: TestSnapshotResults,
testArtifact?: mixed,
// location: string,
};
@@ -315,7 +315,6 @@ function runSpec(spec: Spec): TestCaseResult {
failureDetails: [],
numPassingAsserts: 0,
snapshotResults: {},
testArtifact: null,
};
if (!shouldRunSuite(spec)) {
@@ -330,7 +329,7 @@ function runSpec(spec: Spec): TestCaseResult {
try {
invokeHooks(spec.parentContext, 'beforeEachHooks');
result.testArtifact = spec.implementation();
spec.implementation();
invokeHooks(spec.parentContext, 'afterEachHooks');
status = 'passed';
@@ -402,6 +401,13 @@ function reportTestSuiteResult(testSuiteResult: TestSuiteResult): void {
);
}
export function reportBenchmarkResult(result: BenchmarkResult): void {
// Force the import of the native module to be lazy
const NativeFantom =
require('react-native/src/private/testing/fantom/specs/NativeFantom').default;
NativeFantom.reportTestSuiteResultsJSON(JSON.stringify(result));
}
function validateEmptyMessageQueue(): void {
// Force the import of the native module to be lazy
const NativeFantom =
+24 -3
View File
@@ -8,6 +8,7 @@
* @format
*/
import {reportBenchmarkResult} from '../runtime/setup';
import {getConstants} from './index';
import nullthrows from 'nullthrows';
import NativeCPUTime from 'react-native/src/private/testing/fantom/specs/NativeCPUTime';
@@ -30,6 +31,23 @@ export type SuiteOptions = $ReadOnly<{
export type TestOptions = FnOptions;
export type TestTaskTiming = {
name: string,
latency: {
mean: number,
min: number,
max: number,
p50?: number,
p75?: number,
p99?: number,
},
};
export type BenchmarkResult = {
type: string,
timings: $ReadOnlyArray<TestTaskTiming>,
};
type InternalTestOptions = $ReadOnly<{
...FnOptions,
only?: boolean,
@@ -176,7 +194,7 @@ export function suite(
'Failing focused test to prevent it from being committed',
);
}
return createBenchmarkTestArtifact(bench, tasks);
reportBenchmarkResult(createBenchmarkResultsObject(bench, tasks));
});
const test = (
@@ -255,9 +273,12 @@ function printBenchmarkResults(bench: Bench) {
console.log('');
}
function createBenchmarkTestArtifact(bench: Bench, tasks: Array<TestTask>) {
function createBenchmarkResultsObject(
bench: Bench,
tasks: Array<TestTask>,
): BenchmarkResult {
return {
type: 'benchmark',
type: 'benchmark-result',
timings: tasks.map((task, i) => {
const result = bench.results[i];
const {min, max, mean, p50, p75, p99} = result.latency;
+4 -4
View File
@@ -4126,10 +4126,10 @@ electron-to-chromium@^1.5.73:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.113.tgz#1175b8ba4170541e44e9afa8b992e5bbfff0d150"
integrity sha512-wjT2O4hX+wdWPJ76gWSkMhcHAV2PTMX+QetUCPYEdCIe+cxmgzzSSiGRCKW8nuh4mwKZlpv0xvoW7OF2X+wmHg==
electron@36.3.0:
version "36.3.0"
resolved "https://registry.yarnpkg.com/electron/-/electron-36.3.0.tgz#2f08fb25fb57338e80f33b324ed292f8d3c05b25"
integrity sha512-xk5AJtjxfFRAmehcChWSz/duxI+bJiUCIkH2IX2z8K1SyGsWdpqg+NoJ20JRQ6dpSJw89KXH6juESp8nSyBexA==
electron@37.2.4:
version "37.2.4"
resolved "https://registry.yarnpkg.com/electron/-/electron-37.2.4.tgz#c8458dfbdbbe99753e4a6711b1b86a90338c7198"
integrity sha512-F1WDDvY60TpFwGyW+evNB5q0Em8PamcDTVIKB2NaiaKEbNC2Fabn8Wyxy5g+Anirr1K40eKGjfSJhWEUbI1TOw==
dependencies:
"@electron/get" "^2.0.0"
"@types/node" "^22.7.7"