diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 7fcdaaeec16..aa1d7eed924 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<2143f0faa5bbc2cc77e304adc3edf65c>> */ /** @@ -174,6 +174,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun enableEagerRootViewAttachment(): Boolean = accessor.enableEagerRootViewAttachment() + /** + * Draws ellipsized text on Android without the text hidden after the ellipsis, so that it is not shaped again on every draw. + */ + @JvmStatic + public fun enableEllipsizedTextTrimmingAndroid(): Boolean = accessor.enableEllipsizedTextTrimmingAndroid() + /** * When enabled, Android will disable Props 1.5 raw value merging when Props 2.0 is available. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index 6e0da4d9d0e..3107e7bbfab 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -44,6 +44,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var enableDestroyShadowTreeRevisionAsyncCache: Boolean? = null private var enableDoubleMeasurementFixAndroidCache: Boolean? = null private var enableEagerRootViewAttachmentCache: Boolean? = null + private var enableEllipsizedTextTrimmingAndroidCache: Boolean? = null private var enableExclusivePropsUpdateAndroidCache: Boolean? = null private var enableFabricCommitBranchingCache: Boolean? = null private var enableFabricCommitBranchingMergeOnMainThreadCache: Boolean? = null @@ -334,6 +335,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun enableEllipsizedTextTrimmingAndroid(): Boolean { + var cached = enableEllipsizedTextTrimmingAndroidCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.enableEllipsizedTextTrimmingAndroid() + enableEllipsizedTextTrimmingAndroidCache = cached + } + return cached + } + override fun enableExclusivePropsUpdateAndroid(): Boolean { var cached = enableExclusivePropsUpdateAndroidCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 2268d03f367..1a08f42fd88 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6d86d4adbc261c9271b5bed72bd12dcf>> + * @generated SignedSource<<2f52b0ca227f2fc6e797caf49e810082>> */ /** @@ -76,6 +76,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun enableEagerRootViewAttachment(): Boolean + @DoNotStrip @JvmStatic public external fun enableEllipsizedTextTrimmingAndroid(): Boolean + @DoNotStrip @JvmStatic public external fun enableExclusivePropsUpdateAndroid(): Boolean @DoNotStrip @JvmStatic public external fun enableFabricCommitBranching(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index defafe46f6b..e0c9fcc3c70 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -71,6 +71,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun enableEagerRootViewAttachment(): Boolean = false + override fun enableEllipsizedTextTrimmingAndroid(): Boolean = false + override fun enableExclusivePropsUpdateAndroid(): Boolean = false override fun enableFabricCommitBranching(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 53af4b4edab..01846f0d720 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<40431db1f1e82e88b4209490990137dd>> */ /** @@ -48,6 +48,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var enableDestroyShadowTreeRevisionAsyncCache: Boolean? = null private var enableDoubleMeasurementFixAndroidCache: Boolean? = null private var enableEagerRootViewAttachmentCache: Boolean? = null + private var enableEllipsizedTextTrimmingAndroidCache: Boolean? = null private var enableExclusivePropsUpdateAndroidCache: Boolean? = null private var enableFabricCommitBranchingCache: Boolean? = null private var enableFabricCommitBranchingMergeOnMainThreadCache: Boolean? = null @@ -362,6 +363,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun enableEllipsizedTextTrimmingAndroid(): Boolean { + var cached = enableEllipsizedTextTrimmingAndroidCache + if (cached == null) { + cached = currentProvider.enableEllipsizedTextTrimmingAndroid() + accessedFeatureFlags.add("enableEllipsizedTextTrimmingAndroid") + enableEllipsizedTextTrimmingAndroidCache = cached + } + return cached + } + override fun enableExclusivePropsUpdateAndroid(): Boolean { var cached = enableExclusivePropsUpdateAndroidCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 8fa2f426946..7b4fdce8418 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<02e60f60410efd918ad09ffc46a3bd3d>> */ /** @@ -71,6 +71,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun enableEagerRootViewAttachment(): Boolean + @DoNotStrip public fun enableEllipsizedTextTrimmingAndroid(): Boolean + @DoNotStrip public fun enableExclusivePropsUpdateAndroid(): Boolean @DoNotStrip public fun enableFabricCommitBranching(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EllipsisTrimmedLayout.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EllipsisTrimmedLayout.kt new file mode 100644 index 00000000000..f9d9902f3f8 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/EllipsisTrimmedLayout.kt @@ -0,0 +1,178 @@ +/* + * 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.SpannableString +import android.text.Spanned +import android.text.style.ClickableSpan +import android.text.style.ReplacementSpan +import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.views.text.internal.span.CustomLetterSpacingSpan +import com.facebook.react.views.text.internal.span.TextEffectSpan +import com.facebook.react.views.text.internal.span.TouchableSpan + +/** + * A delegating [Layout] over the text of a [delegate] ellipsized at the end, without the text + * hidden after the ellipsis. + * + * The last line of an ellipsized layout keeps all the text after the ellipsis, as zero-width + * characters, and Android shapes all of it every time the layout is drawn. This layout's text stops + * right after the ellipsis character, and its line metrics are delegated to [delegate], like in + * [MutableSpannableLayout], so it draws the same visible text without laying anything out again. + */ +internal class EllipsisTrimmedLayout +private constructor( + private val delegate: Layout, + trimmedText: SpannableString, +) : + Layout( + trimmedText, + delegate.paint, + delegate.width, + delegate.alignment, + delegate.spacingMultiplier, + delegate.spacingAdd, + ) { + + private val lastLine = delegate.lineCount - 1 + + companion object { + /** + * Returns an [EllipsisTrimmedLayout] if [layout], a layout of [source], is ellipsized at the + * end and hides text that only matters for drawing, else null. + */ + @OptIn(UnstableReactNativeAPI::class) + fun createIfNeeded( + layout: Layout, + source: Spanned, + hyphenationFrequency: Int, + justificationMode: Int, + ): EllipsisTrimmedLayout? { + // Justified lines and hyphens are drawn from the layout's justification mode and hyphen + // edits, which a delegating layout doesn't have. + if ( + hyphenationFrequency != Layout.HYPHENATION_FREQUENCY_NONE || + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + justificationMode != Layout.JUSTIFICATION_MODE_NONE) + ) { + return null + } + val lastLine = layout.lineCount - 1 + if (lastLine < 0 || layout.getEllipsisCount(lastLine) <= 1) { + return null + } + // The ellipsis character is the first ellipsized character; the rest of the last line is + // hidden, and so are the paragraphs after it. Text after the ellipsized range (start or + // middle ellipsis) is visible, so keep it. + val ellipsisStart = layout.getLineStart(lastLine) + layout.getEllipsisStart(lastLine) + if (ellipsisStart + layout.getEllipsisCount(lastLine) != layout.getLineEnd(lastLine)) { + return null + } + val end = ellipsisStart + 1 + // Keep the text whole when its hidden part matters beyond drawing. Bidirectional text is laid + // out in runs over the whole line, which can put hidden runs before visible ones, and on + // Android 15+ text with right-to-left characters is drawn with bounds-based width, which this + // layout doesn't have. Letter spacing also spaces the hidden characters before Android 15. + if (layout.paint.letterSpacing != 0f || mayNeedBidi(source)) { + return null + } + // Links are exposed to accessibility, replacement spans such as inline views are measured + // and drawn over their whole range, and text effects, touchable spans and letter spacing + // work on the range they were given. + for (span in source.getSpans(end, source.length, Any::class.java)) { + if ( + span is ClickableSpan || + span is ReplacementSpan || + span is TextEffectSpan || + span is TouchableSpan || + span is CustomLetterSpacingSpan + ) { + return null + } + } + // The layout's text already has the ellipsis character in place. + val trimmedText = layout.text.subSequence(0, end) + if (trimmedText[trimmedText.length - 1] != '…') { + return null + } + return EllipsisTrimmedLayout( + layout, + trimmedText as? SpannableString ?: SpannableString(trimmedText), + ) + } + + /** The whole text of [layout], also when it's an [EllipsisTrimmedLayout]. */ + fun untrimmedText(layout: Layout): CharSequence = + ((layout as? EllipsisTrimmedLayout)?.delegate ?: layout).text + + /** + * Whether [text] has a right-to-left character or a directional formatting character. Unlike + * `TextDirectionHeuristics.ANYRTL_LTR`, this also sees right-to-left text inside isolates. + */ + private fun mayNeedBidi(text: CharSequence): Boolean { + var i = 0 + while (i < text.length) { + val codePoint = Character.codePointAt(text, i) + when (Character.getDirectionality(codePoint)) { + Character.DIRECTIONALITY_RIGHT_TO_LEFT, + Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC, + Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING, + Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE, + Character.DIRECTIONALITY_RIGHT_TO_LEFT_ISOLATE, + Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING, + Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE, + Character.DIRECTIONALITY_LEFT_TO_RIGHT_ISOLATE, + Character.DIRECTIONALITY_FIRST_STRONG_ISOLATE, + Character.DIRECTIONALITY_POP_DIRECTIONAL_FORMAT, + Character.DIRECTIONALITY_POP_DIRECTIONAL_ISOLATE -> return true + } + i += Character.charCount(codePoint) + } + return false + } + } + + // --- Abstract methods: delegated, except where the last line ends --- + + override fun getLineCount(): Int = delegate.lineCount + + override fun getLineTop(line: Int): Int = delegate.getLineTop(line) + + override fun getLineDescent(line: Int): Int = delegate.getLineDescent(line) + + override fun getLineStart(line: Int): Int = + if (line > lastLine) text.length else delegate.getLineStart(line) + + override fun getLineContainsTab(line: Int): Boolean = delegate.getLineContainsTab(line) + + override fun getLineDirections(line: Int): Directions = delegate.getLineDirections(line) + + override fun getTopPadding(): Int = delegate.topPadding + + override fun getBottomPadding(): Int = delegate.bottomPadding + + override fun getEllipsisStart(line: Int): Int = delegate.getEllipsisStart(line) + + override fun getEllipsisCount(line: Int): Int = + if (line == lastLine) text.length - getLineStart(line) - getEllipsisStart(line) + else delegate.getEllipsisCount(line) + + override fun getParagraphDirection(line: Int): Int = delegate.getParagraphDirection(line) + + override fun getEllipsizedWidth(): Int = delegate.ellipsizedWidth + + // Line widths (getLineMax, getLineWidth, getLineLeft, getLineRight) are not delegated: the + // original computes them by shaping its whole last line, hidden text included, and this layout + // gets the same values from its own, shorter text. + + // Only called by the framework on API 33+ + @android.annotation.SuppressLint("NewApi") + override fun isFallbackLineSpacingEnabled(): Boolean = delegate.isFallbackLineSpacingEnabled +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt index ef7c3a33b04..14c25f1647a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt @@ -215,7 +215,9 @@ public constructor( preparedLayout: PreparedLayout, ): ReactTextUpdate { val layout = preparedLayout.layout - val text = layout.text + // The TextView lays the text out again, so it needs the whole text, not the drawn text trimmed + // after an ellipsis. + val text = EllipsisTrimmedLayout.untrimmedText(layout) val spanned = if (text is Spannable) text else SpannableString(text) view.setSpanned(spanned) view.setPreparedLayout(preparedLayout) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt index d906e9d78e6..9349bbb75ac 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt @@ -1355,6 +1355,7 @@ internal object TextLayoutManager { return CreateLayoutResult( layout, textBreakStrategy, + hyphenationFrequency, justificationMode, ) } @@ -1439,7 +1440,12 @@ internal object TextLayoutManager { ) return PreparedLayout( - result.layout, + layoutForDrawing( + result.layout, + text, + result.hyphenationFrequency, + result.justificationMode, + ), maximumNumberOfLines, verticalOffset, reactTags, @@ -1451,6 +1457,28 @@ internal object TextLayoutManager { ) } + /** + * The layout a prepared layout draws: [layout] without the text hidden after an ellipsis, when + * that is enabled and possible. + */ + @VisibleForTesting + internal fun layoutForDrawing( + layout: Layout, + text: Spanned, + hyphenationFrequency: Int, + justificationMode: Int, + ): Layout { + if (!ReactNativeFeatureFlags.enableEllipsizedTextTrimmingAndroid()) { + return layout + } + return EllipsisTrimmedLayout.createIfNeeded( + layout, + text, + hyphenationFrequency, + justificationMode, + ) ?: layout + } + @JvmStatic fun adjustSpannableFontToFit( text: Spannable, @@ -1960,6 +1988,7 @@ internal object TextLayoutManager { private class CreateLayoutResult( val layout: Layout, val textBreakStrategy: Int, + val hyphenationFrequency: Int, val justificationMode: Int, ) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index 23fd0978a76..69256891638 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<944781e7a547e601f0637869a3a3ec5e>> + * @generated SignedSource<<7500b475cd644b37a53a4bc9df89494a>> */ /** @@ -183,6 +183,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool enableEllipsizedTextTrimmingAndroid() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableEllipsizedTextTrimmingAndroid"); + return method(javaProvider_); + } + bool enableExclusivePropsUpdateAndroid() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableExclusivePropsUpdateAndroid"); @@ -745,6 +751,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableEagerRootViewAttachment( return ReactNativeFeatureFlags::enableEagerRootViewAttachment(); } +bool JReactNativeFeatureFlagsCxxInterop::enableEllipsizedTextTrimmingAndroid( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::enableEllipsizedTextTrimmingAndroid(); +} + bool JReactNativeFeatureFlagsCxxInterop::enableExclusivePropsUpdateAndroid( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::enableExclusivePropsUpdateAndroid(); @@ -1213,6 +1224,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "enableEagerRootViewAttachment", JReactNativeFeatureFlagsCxxInterop::enableEagerRootViewAttachment), + makeNativeMethod( + "enableEllipsizedTextTrimmingAndroid", + JReactNativeFeatureFlagsCxxInterop::enableEllipsizedTextTrimmingAndroid), makeNativeMethod( "enableExclusivePropsUpdateAndroid", JReactNativeFeatureFlagsCxxInterop::enableExclusivePropsUpdateAndroid), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index ae774f80c82..ebda1864feb 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<046411b1c1b422854597ec5c0cfadcd1>> */ /** @@ -102,6 +102,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool enableEagerRootViewAttachment( facebook::jni::alias_ref); + static bool enableEllipsizedTextTrimmingAndroid( + facebook::jni::alias_ref); + static bool enableExclusivePropsUpdateAndroid( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/EllipsisTrimmedLayoutTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/EllipsisTrimmedLayoutTest.kt new file mode 100644 index 00000000000..2758370e729 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/EllipsisTrimmedLayoutTest.kt @@ -0,0 +1,404 @@ +/* + * 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. + */ + +@file:OptIn(UnstableReactNativeAPI::class) + +package com.facebook.react.views.text + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Typeface +import android.graphics.text.LineBreakConfig +import android.text.Layout +import android.text.Spannable +import android.text.SpannableString +import android.text.TextPaint +import android.text.TextUtils +import android.text.style.ClickableSpan +import android.text.style.ForegroundColorSpan +import android.text.style.ReplacementSpan +import android.text.style.StyleSpan +import android.view.View +import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.views.text.internal.span.CustomLetterSpacingSpan +import com.facebook.react.views.text.internal.span.StatefulSpan +import com.facebook.react.views.text.internal.span.TextEffectSpan +import com.facebook.react.views.text.internal.span.TextInlineViewPlaceholderSpan +import com.facebook.react.views.text.internal.span.TouchableSpan +import com.facebook.yoga.YogaMeasureMode +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.annotation.Config +import org.robolectric.annotation.GraphicsMode + +@RunWith(RobolectricTestRunner::class) +@SuppressLint("NewApi") +@Config(sdk = [28, 30, 34, 35]) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +class EllipsisTrimmedLayoutTest { + + @Before + fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() + } + + @After + fun tearDown() { + ReactNativeFeatureFlags.dangerouslyReset() + } + + private val longText = (1..40).joinToString(" ") { "word$it" } + + @Test + fun `one ellipsized line keeps only the text up to the ellipsis`() { + val text = SpannableString(longText) + val layout = createLayout(text, maxLines = 1) + + val trimmed = checkNotNull(trim(layout, text)) + + assertThat(trimmed.text.length).isEqualTo(layout.getEllipsisStart(0) + 1) + assertThat(trimmed.text[trimmed.text.length - 1]).isEqualTo('…') + assertThat(EllipsisTrimmedLayout.untrimmedText(trimmed)).isSameAs(layout.text) + assertThat(EllipsisTrimmedLayout.untrimmedText(layout)).isSameAs(layout.text) + assertSameLayout(trimmed, layout) + } + + @Test + fun `several ellipsized lines with styled text crossing the ellipsis draw the same`() { + val text = SpannableString(longText) + text.setSpan(StyleSpan(Typeface.BOLD), 20, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + text.setSpan( + ForegroundColorSpan(Color.RED), + 40, + text.length - 10, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + val layout = createLayout(text, maxLines = 3) + + val trimmed = checkNotNull(trim(layout, text)) + + assertThat(trimmed.text.length).isLessThan(text.length) + assertSameLayout(trimmed, layout) + } + + @Test + fun `hidden text with line breaks draws the same`() { + val text = SpannableString(longText + "\n" + longText + "\n" + longText) + val layout = createLayout(text, maxLines = 2) + + val trimmed = checkNotNull(trim(layout, text)) + + assertThat(trimmed.text.length).isLessThan(text.length) + assertSameLayout(trimmed, layout) + } + + @Test + fun `centered and end-aligned text draws the same`() { + for (alignment in listOf(Layout.Alignment.ALIGN_CENTER, Layout.Alignment.ALIGN_OPPOSITE)) { + val text = SpannableString(longText) + val layout = createLayout(text, maxLines = 2, alignment = alignment) + + val trimmed = checkNotNull(trim(layout, text)) + + assertSameLayout(trimmed, layout) + } + } + + @Test + fun `text with letter spacing is kept`() { + // Before Android 15, letter spacing also spaces the hidden characters. + for (alignment in listOf(Layout.Alignment.ALIGN_NORMAL, Layout.Alignment.ALIGN_CENTER)) { + val spanned = SpannableString(longText) + spanned.setSpan( + CustomLetterSpacingSpan(0.1f), + spanned.length - 5, + spanned.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + val spannedLayout = createLayout(spanned, maxLines = 2, alignment = alignment) + assertThat(trim(spannedLayout, spanned)).isNull() + + val text = SpannableString(longText) + val layout = createLayout(text, maxLines = 2, alignment = alignment, textPaint = spacedPaint) + assertThat(trim(layout, text)).isNull() + } + } + + @Test + fun `justified or hyphenated text is kept`() { + val text = SpannableString(longText) + val layout = createLayout(text, maxLines = 2) + + assertThat(trim(layout, text)).isNotNull() + assertThat(trim(layout, text, justificationMode = Layout.JUSTIFICATION_MODE_INTER_WORD)) + .isNull() + assertThat(trim(layout, text, hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NORMAL)) + .isNull() + } + + @Test + fun `text that is not ellipsized is kept`() { + val text = SpannableString("word1 word2") + val layout = createLayout(text, maxLines = 1) + + assertThat(trim(layout, text)).isNull() + } + + @Test + fun `text whose ellipsis hides nothing else is kept`() { + val text = SpannableString("word1\nword2") + val layout = createLayout(text, maxLines = 1) + + assertThat(trim(layout, text)).isNull() + } + + @Test + fun `text ellipsized in the middle is kept`() { + val text = SpannableString(longText) + val layout = createLayout(text, maxLines = 1, ellipsizeMode = TextUtils.TruncateAt.MIDDLE) + + assertThat(layout.getEllipsisCount(0)).isGreaterThan(1) + assertThat(trim(layout, text)).isNull() + } + + @Test + fun `right-to-left text is kept`() { + val text = SpannableString((1..40).joinToString(" ") { "שלום$it" }) + val layout = createLayout(text, maxLines = 1) + + assertThat(trim(layout, text)).isNull() + } + + @Test + fun `right-to-left text inside an isolate is kept`() { + // TextDirectionHeuristics.ANYRTL_LTR skips isolates, but their runs are still reordered. + val text = SpannableString("From \u2068محمد 2024 السعيد\u2069 $longText") + val layout = createLayout(text, maxLines = 1) + + assertThat(layout.getEllipsisCount(0)).isGreaterThan(1) + assertThat(trim(layout, text)).isNull() + } + + @Test + fun `right-to-left characters only in the hidden text keep the whole text`() { + // Android 15+ draws text with right-to-left characters anywhere with bounds-based width. + val text = SpannableString("$longText\n${(1..10).joinToString(" ") { "שלום$it" }}") + val layout = createLayout(text, maxLines = 1) + + assertThat(layout.getEllipsisCount(0)).isGreaterThan(1) + assertThat(trim(layout, text)).isNull() + } + + @Test + fun `a link in the visible text is kept and one in the hidden text keeps the whole text`() { + val visible = SpannableString(longText) + visible.setSpan(ClickableLink(), 0, 5, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + val trimmed = checkNotNull(trim(createLayout(visible, maxLines = 1), visible)) + val trimmedText = trimmed.text as Spannable + assertThat(trimmedText.getSpans(0, trimmedText.length, ClickableLink::class.java)).hasSize(1) + + assertHiddenSpanKeepsText(ClickableLink()) + + // A link that starts in the visible text and continues into the hidden text. + val crossing = SpannableString(longText) + crossing.setSpan(ClickableLink(), 5, crossing.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + assertThat(trim(createLayout(crossing, maxLines = 1), crossing)).isNull() + } + + @Test + fun `an inline view in the hidden text keeps the whole text`() { + assertHiddenSpanKeepsText( + TextInlineViewPlaceholderSpan(/* reactTag= */ 1, /* width= */ 10, /* height= */ 10) + ) + } + + @Test + fun `a replacement span in the hidden text keeps the whole text`() { + assertHiddenSpanKeepsText( + object : ReplacementSpan() { + override fun getSize( + paint: Paint, + text: CharSequence?, + start: Int, + end: Int, + fm: Paint.FontMetricsInt?, + ): Int = 10 + + override fun draw( + canvas: Canvas, + text: CharSequence?, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint, + ) = Unit + } + ) + } + + @Test + fun `stateful spans in the visible text draw the same through MutableSpannableLayout`() { + val text = SpannableString(longText) + text.setSpan(ColorStatefulSpan(), 0, 10, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + val layout = createLayout(text, maxLines = 2) + val trimmed = checkNotNull(trim(layout, text)) + + val original = checkNotNull(MutableSpannableLayout.createIfNeeded(layout)) + val mutableTrimmed = checkNotNull(MutableSpannableLayout.createIfNeeded(trimmed)) + + assertSameLayout(mutableTrimmed, original) + } + + @Test + fun `prepared layouts are trimmed only with the feature flag`() { + val text = SpannableString(longText) + val layout = createLayout(text, maxLines = 2) + val none = Layout.HYPHENATION_FREQUENCY_NONE + val noJustification = Layout.JUSTIFICATION_MODE_NONE + + assertThat(TextLayoutManager.layoutForDrawing(layout, text, none, noJustification)) + .isSameAs(layout) + + ReactNativeFeatureFlags.dangerouslyReset() + ReactNativeFeatureFlags.override( + object : ReactNativeFeatureFlagsDefaults() { + override fun enableEllipsizedTextTrimmingAndroid(): Boolean = true + } + ) + assertThat(TextLayoutManager.layoutForDrawing(layout, text, none, noJustification)) + .isInstanceOf(EllipsisTrimmedLayout::class.java) + assertThat( + TextLayoutManager.layoutForDrawing( + layout, + text, + Layout.HYPHENATION_FREQUENCY_NORMAL, + noJustification, + ) + ) + .isSameAs(layout) + } + + @Test + fun `a text effect in the hidden text keeps the whole text`() { + assertHiddenSpanKeepsText(object : TextEffectSpan {}) + } + + @Test + fun `a touchable span in the hidden text keeps the whole text`() { + assertHiddenSpanKeepsText( + object : TouchableSpan { + override fun onTouchEvent(action: Int, layoutX: Float, layoutY: Float): Boolean = false + } + ) + } + + private fun trim( + layout: Layout, + text: Spannable, + hyphenationFrequency: Int = Layout.HYPHENATION_FREQUENCY_NONE, + justificationMode: Int = Layout.JUSTIFICATION_MODE_NONE, + ): EllipsisTrimmedLayout? = + EllipsisTrimmedLayout.createIfNeeded(layout, text, hyphenationFrequency, justificationMode) + + private fun assertHiddenSpanKeepsText(span: Any) { + val text = SpannableString(longText) + text.setSpan(span, text.length - 1, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + val layout = createLayout(text, maxLines = 1) + + assertThat(layout.getEllipsisCount(0)).isGreaterThan(1) + assertThat(trim(layout, text)).isNull() + } + + private fun assertSameLayout(trimmed: Layout, layout: Layout) { + assertThat(trimmed.lineCount).isEqualTo(layout.lineCount) + assertThat(trimmed.height).isEqualTo(layout.height) + for (line in 0 until layout.lineCount) { + assertThat(trimmed.getLineStart(line)).isEqualTo(layout.getLineStart(line)) + assertThat(trimmed.getEllipsisStart(line)).isEqualTo(layout.getEllipsisStart(line)) + assertThat(trimmed.getLineBaseline(line)).isEqualTo(layout.getLineBaseline(line)) + assertThat(trimmed.getLineWidth(line)).isEqualTo(layout.getLineWidth(line)) + } + val lastLine = layout.lineCount - 1 + assertThat(trimmed.getLineEnd(lastLine)).isEqualTo(trimmed.text.length) + // Touches on the last line, including on the ellipsis, land on the same characters. + val lineLeft = layout.getLineLeft(lastLine).toInt() + val lineRight = layout.getLineRight(lastLine).toInt() + for (x in lineLeft..lineRight step 5) { + assertThat(trimmed.getOffsetForHorizontal(lastLine, x.toFloat())) + .isEqualTo( + minOf(layout.getOffsetForHorizontal(lastLine, x.toFloat()), trimmed.text.length) + ) + } + val drawn = draw(layout) + assertThat(drawn.sameAs(draw(trimmed))).isTrue() + // Make sure that text was drawn at all. + assertThat( + (0 until drawn.height).any { y -> + (0 until drawn.width).any { x -> drawn.getPixel(x, y) != Color.WHITE } + } + ) + .isTrue() + } + + private fun draw(layout: Layout): Bitmap { + val bitmap = Bitmap.createBitmap(layout.width, layout.height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + canvas.drawColor(Color.WHITE) + layout.draw(canvas) + return bitmap + } + + private fun createLayout( + text: Spannable, + maxLines: Int, + ellipsizeMode: TextUtils.TruncateAt = TextUtils.TruncateAt.END, + alignment: Layout.Alignment = Layout.Alignment.ALIGN_NORMAL, + textPaint: TextPaint = paint, + ): Layout = + TextLayoutManager.createLayout( + text, + /* boring = */ null, + /* width = */ 400f, + YogaMeasureMode.EXACTLY, + /* includeFontPadding = */ false, + /* textBreakStrategy = */ Layout.BREAK_STRATEGY_HIGH_QUALITY, + /* hyphenationFrequency = */ Layout.HYPHENATION_FREQUENCY_NONE, + /* lineBreakStyle = */ LineBreakConfig.LINE_BREAK_STYLE_NONE, + /* lineBreakWordStyle = */ LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE, + alignment, + Layout.JUSTIFICATION_MODE_NONE, + ellipsizeMode, + maxLines, + textPaint, + ) + + private class ClickableLink : ClickableSpan() { + override fun onClick(widget: View) = Unit + } + + private class ColorStatefulSpan : ForegroundColorSpan(Color.BLUE), StatefulSpan { + override fun clone(): StatefulSpan = ColorStatefulSpan() + } + + private companion object { + val paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG).apply { textSize = 26f } + val spacedPaint = TextPaint(paint).apply { letterSpacing = 0.1f } + } +} diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index ac8ddb96a11..b5ce26da3a4 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<56e127471f63e47798ec619e6fa56738>> + * @generated SignedSource<<1cd242f72f95aaf95ea05fa25aea7cdf>> */ /** @@ -122,6 +122,10 @@ bool ReactNativeFeatureFlags::enableEagerRootViewAttachment() { return getAccessor().enableEagerRootViewAttachment(); } +bool ReactNativeFeatureFlags::enableEllipsizedTextTrimmingAndroid() { + return getAccessor().enableEllipsizedTextTrimmingAndroid(); +} + bool ReactNativeFeatureFlags::enableExclusivePropsUpdateAndroid() { return getAccessor().enableExclusivePropsUpdateAndroid(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 0e5b3f2013f..835a6fa5ac2 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6887b671526bc8a3b0678e3e45b4e3b4>> + * @generated SignedSource<> */ /** @@ -161,6 +161,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool enableEagerRootViewAttachment(); + /** + * Draws ellipsized text on Android without the text hidden after the ellipsis, so that it is not shaped again on every draw. + */ + RN_EXPORT static bool enableEllipsizedTextTrimmingAndroid(); + /** * When enabled, Android will disable Props 1.5 raw value merging when Props 2.0 is available. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 7640cea6418..52e6b49dd71 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5d9712bd7e62ba3a29b084ba4c60bec5>> + * @generated SignedSource<> */ /** @@ -461,6 +461,24 @@ bool ReactNativeFeatureFlagsAccessor::enableEagerRootViewAttachment() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::enableEllipsizedTextTrimmingAndroid() { + auto flagValue = enableEllipsizedTextTrimmingAndroid_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(24, "enableEllipsizedTextTrimmingAndroid"); + + flagValue = currentProvider_->enableEllipsizedTextTrimmingAndroid(); + enableEllipsizedTextTrimmingAndroid_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::enableExclusivePropsUpdateAndroid() { auto flagValue = enableExclusivePropsUpdateAndroid_.load(); @@ -470,7 +488,7 @@ bool ReactNativeFeatureFlagsAccessor::enableExclusivePropsUpdateAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(24, "enableExclusivePropsUpdateAndroid"); + markFlagAsAccessed(25, "enableExclusivePropsUpdateAndroid"); flagValue = currentProvider_->enableExclusivePropsUpdateAndroid(); enableExclusivePropsUpdateAndroid_ = flagValue; @@ -488,7 +506,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricCommitBranching() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(25, "enableFabricCommitBranching"); + markFlagAsAccessed(26, "enableFabricCommitBranching"); flagValue = currentProvider_->enableFabricCommitBranching(); enableFabricCommitBranching_ = flagValue; @@ -506,7 +524,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricCommitBranchingMergeOnMainThre // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(26, "enableFabricCommitBranchingMergeOnMainThread"); + markFlagAsAccessed(27, "enableFabricCommitBranchingMergeOnMainThread"); flagValue = currentProvider_->enableFabricCommitBranchingMergeOnMainThread(); enableFabricCommitBranchingMergeOnMainThread_ = flagValue; @@ -524,7 +542,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricLogs() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(27, "enableFabricLogs"); + markFlagAsAccessed(28, "enableFabricLogs"); flagValue = currentProvider_->enableFabricLogs(); enableFabricLogs_ = flagValue; @@ -542,7 +560,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFlexboxAutoMinSizeInStrictMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(28, "enableFlexboxAutoMinSizeInStrictMode"); + markFlagAsAccessed(29, "enableFlexboxAutoMinSizeInStrictMode"); flagValue = currentProvider_->enableFlexboxAutoMinSizeInStrictMode(); enableFlexboxAutoMinSizeInStrictMode_ = flagValue; @@ -560,7 +578,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFollowUpMountItemDispatchAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(29, "enableFollowUpMountItemDispatchAndroid"); + markFlagAsAccessed(30, "enableFollowUpMountItemDispatchAndroid"); flagValue = currentProvider_->enableFollowUpMountItemDispatchAndroid(); enableFollowUpMountItemDispatchAndroid_ = flagValue; @@ -578,7 +596,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSCompressedTextFrameAdjustment() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(30, "enableIOSCompressedTextFrameAdjustment"); + markFlagAsAccessed(31, "enableIOSCompressedTextFrameAdjustment"); flagValue = currentProvider_->enableIOSCompressedTextFrameAdjustment(); enableIOSCompressedTextFrameAdjustment_ = flagValue; @@ -596,7 +614,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSTextBaselineOffsetPerLine() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(31, "enableIOSTextBaselineOffsetPerLine"); + markFlagAsAccessed(32, "enableIOSTextBaselineOffsetPerLine"); flagValue = currentProvider_->enableIOSTextBaselineOffsetPerLine(); enableIOSTextBaselineOffsetPerLine_ = flagValue; @@ -614,7 +632,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSViewClipToPaddingBox() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(32, "enableIOSViewClipToPaddingBox"); + markFlagAsAccessed(33, "enableIOSViewClipToPaddingBox"); flagValue = currentProvider_->enableIOSViewClipToPaddingBox(); enableIOSViewClipToPaddingBox_ = flagValue; @@ -632,7 +650,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImagePrefetchingAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(33, "enableImagePrefetchingAndroid"); + markFlagAsAccessed(34, "enableImagePrefetchingAndroid"); flagValue = currentProvider_->enableImagePrefetchingAndroid(); enableImagePrefetchingAndroid_ = flagValue; @@ -650,7 +668,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImageTransparentTintColor() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(34, "enableImageTransparentTintColor"); + markFlagAsAccessed(35, "enableImageTransparentTintColor"); flagValue = currentProvider_->enableImageTransparentTintColor(); enableImageTransparentTintColor_ = flagValue; @@ -668,7 +686,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImmediateUpdateModeForContentOffsetC // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(35, "enableImmediateUpdateModeForContentOffsetChanges"); + markFlagAsAccessed(36, "enableImmediateUpdateModeForContentOffsetChanges"); flagValue = currentProvider_->enableImmediateUpdateModeForContentOffsetChanges(); enableImmediateUpdateModeForContentOffsetChanges_ = flagValue; @@ -686,7 +704,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImperativeEvents() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(36, "enableImperativeEvents"); + markFlagAsAccessed(37, "enableImperativeEvents"); flagValue = currentProvider_->enableImperativeEvents(); enableImperativeEvents_ = flagValue; @@ -704,7 +722,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImperativeFocus() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(37, "enableImperativeFocus"); + markFlagAsAccessed(38, "enableImperativeFocus"); flagValue = currentProvider_->enableImperativeFocus(); enableImperativeFocus_ = flagValue; @@ -722,7 +740,7 @@ bool ReactNativeFeatureFlagsAccessor::enableInteropViewManagerClassLookUpOptimiz // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(38, "enableInteropViewManagerClassLookUpOptimizationIOS"); + markFlagAsAccessed(39, "enableInteropViewManagerClassLookUpOptimizationIOS"); flagValue = currentProvider_->enableInteropViewManagerClassLookUpOptimizationIOS(); enableInteropViewManagerClassLookUpOptimizationIOS_ = flagValue; @@ -740,7 +758,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIntersectionObserverByDefault() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(39, "enableIntersectionObserverByDefault"); + markFlagAsAccessed(40, "enableIntersectionObserverByDefault"); flagValue = currentProvider_->enableIntersectionObserverByDefault(); enableIntersectionObserverByDefault_ = flagValue; @@ -758,7 +776,7 @@ bool ReactNativeFeatureFlagsAccessor::enableKeyEvents() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(40, "enableKeyEvents"); + markFlagAsAccessed(41, "enableKeyEvents"); flagValue = currentProvider_->enableKeyEvents(); enableKeyEvents_ = flagValue; @@ -776,7 +794,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(41, "enableLayoutAnimationsOnAndroid"); + markFlagAsAccessed(42, "enableLayoutAnimationsOnAndroid"); flagValue = currentProvider_->enableLayoutAnimationsOnAndroid(); enableLayoutAnimationsOnAndroid_ = flagValue; @@ -794,7 +812,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnIOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(42, "enableLayoutAnimationsOnIOS"); + markFlagAsAccessed(43, "enableLayoutAnimationsOnIOS"); flagValue = currentProvider_->enableLayoutAnimationsOnIOS(); enableLayoutAnimationsOnIOS_ = flagValue; @@ -812,7 +830,7 @@ bool ReactNativeFeatureFlagsAccessor::enableModuleArgumentNSNullConversionIOS() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(43, "enableModuleArgumentNSNullConversionIOS"); + markFlagAsAccessed(44, "enableModuleArgumentNSNullConversionIOS"); flagValue = currentProvider_->enableModuleArgumentNSNullConversionIOS(); enableModuleArgumentNSNullConversionIOS_ = flagValue; @@ -830,7 +848,7 @@ bool ReactNativeFeatureFlagsAccessor::enableMountingCoordinatorPullModelAndroid( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(44, "enableMountingCoordinatorPullModelAndroid"); + markFlagAsAccessed(45, "enableMountingCoordinatorPullModelAndroid"); flagValue = currentProvider_->enableMountingCoordinatorPullModelAndroid(); enableMountingCoordinatorPullModelAndroid_ = flagValue; @@ -848,7 +866,7 @@ bool ReactNativeFeatureFlagsAccessor::enableMutationObserverByDefault() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(45, "enableMutationObserverByDefault"); + markFlagAsAccessed(46, "enableMutationObserverByDefault"); flagValue = currentProvider_->enableMutationObserverByDefault(); enableMutationObserverByDefault_ = flagValue; @@ -866,7 +884,7 @@ bool ReactNativeFeatureFlagsAccessor::enableNativeCSSParsing() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(46, "enableNativeCSSParsing"); + markFlagAsAccessed(47, "enableNativeCSSParsing"); flagValue = currentProvider_->enableNativeCSSParsing(); enableNativeCSSParsing_ = flagValue; @@ -884,7 +902,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePreallocatedPropsDiffOnInsertAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(47, "enablePreallocatedPropsDiffOnInsertAndroid"); + markFlagAsAccessed(48, "enablePreallocatedPropsDiffOnInsertAndroid"); flagValue = currentProvider_->enablePreallocatedPropsDiffOnInsertAndroid(); enablePreallocatedPropsDiffOnInsertAndroid_ = flagValue; @@ -902,7 +920,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePreparedTextLayout() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(48, "enablePreparedTextLayout"); + markFlagAsAccessed(49, "enablePreparedTextLayout"); flagValue = currentProvider_->enablePreparedTextLayout(); enablePreparedTextLayout_ = flagValue; @@ -920,7 +938,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePropsUpdateReconciliationAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(49, "enablePropsUpdateReconciliationAndroid"); + markFlagAsAccessed(50, "enablePropsUpdateReconciliationAndroid"); flagValue = currentProvider_->enablePropsUpdateReconciliationAndroid(); enablePropsUpdateReconciliationAndroid_ = flagValue; @@ -938,7 +956,7 @@ bool ReactNativeFeatureFlagsAccessor::enableResizeObserverByDefault() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(50, "enableResizeObserverByDefault"); + markFlagAsAccessed(51, "enableResizeObserverByDefault"); flagValue = currentProvider_->enableResizeObserverByDefault(); enableResizeObserverByDefault_ = flagValue; @@ -956,7 +974,7 @@ bool ReactNativeFeatureFlagsAccessor::enableSwiftUIBasedFilters() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(51, "enableSwiftUIBasedFilters"); + markFlagAsAccessed(52, "enableSwiftUIBasedFilters"); flagValue = currentProvider_->enableSwiftUIBasedFilters(); enableSwiftUIBasedFilters_ = flagValue; @@ -974,7 +992,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewCulling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(52, "enableViewCulling"); + markFlagAsAccessed(53, "enableViewCulling"); flagValue = currentProvider_->enableViewCulling(); enableViewCulling_ = flagValue; @@ -992,7 +1010,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecycling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(53, "enableViewRecycling"); + markFlagAsAccessed(54, "enableViewRecycling"); flagValue = currentProvider_->enableViewRecycling(); enableViewRecycling_ = flagValue; @@ -1010,7 +1028,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForImage() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(54, "enableViewRecyclingForImage"); + markFlagAsAccessed(55, "enableViewRecyclingForImage"); flagValue = currentProvider_->enableViewRecyclingForImage(); enableViewRecyclingForImage_ = flagValue; @@ -1028,7 +1046,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForScrollView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(55, "enableViewRecyclingForScrollView"); + markFlagAsAccessed(56, "enableViewRecyclingForScrollView"); flagValue = currentProvider_->enableViewRecyclingForScrollView(); enableViewRecyclingForScrollView_ = flagValue; @@ -1046,7 +1064,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForText() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(56, "enableViewRecyclingForText"); + markFlagAsAccessed(57, "enableViewRecyclingForText"); flagValue = currentProvider_->enableViewRecyclingForText(); enableViewRecyclingForText_ = flagValue; @@ -1064,7 +1082,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(57, "enableViewRecyclingForView"); + markFlagAsAccessed(58, "enableViewRecyclingForView"); flagValue = currentProvider_->enableViewRecyclingForView(); enableViewRecyclingForView_ = flagValue; @@ -1082,7 +1100,7 @@ bool ReactNativeFeatureFlagsAccessor::enableVirtualViewContainerStateExperimenta // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(58, "enableVirtualViewContainerStateExperimental"); + markFlagAsAccessed(59, "enableVirtualViewContainerStateExperimental"); flagValue = currentProvider_->enableVirtualViewContainerStateExperimental(); enableVirtualViewContainerStateExperimental_ = flagValue; @@ -1100,7 +1118,7 @@ bool ReactNativeFeatureFlagsAccessor::fixBorderlessRippleAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(59, "fixBorderlessRippleAndroid"); + markFlagAsAccessed(60, "fixBorderlessRippleAndroid"); flagValue = currentProvider_->fixBorderlessRippleAndroid(); fixBorderlessRippleAndroid_ = flagValue; @@ -1118,7 +1136,7 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(60, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); + markFlagAsAccessed(61, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); flagValue = currentProvider_->fixMappingOfEventPrioritiesBetweenFabricAndReact(); fixMappingOfEventPrioritiesBetweenFabricAndReact_ = flagValue; @@ -1136,7 +1154,7 @@ bool ReactNativeFeatureFlagsAccessor::fixTextLayoutOnPixelDensityChange() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(61, "fixTextLayoutOnPixelDensityChange"); + markFlagAsAccessed(62, "fixTextLayoutOnPixelDensityChange"); flagValue = currentProvider_->fixTextLayoutOnPixelDensityChange(); fixTextLayoutOnPixelDensityChange_ = flagValue; @@ -1154,7 +1172,7 @@ bool ReactNativeFeatureFlagsAccessor::fixYogaFlexBasisFitContentInMainAxis() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(62, "fixYogaFlexBasisFitContentInMainAxis"); + markFlagAsAccessed(63, "fixYogaFlexBasisFitContentInMainAxis"); flagValue = currentProvider_->fixYogaFlexBasisFitContentInMainAxis(); fixYogaFlexBasisFitContentInMainAxis_ = flagValue; @@ -1172,7 +1190,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxAssertSingleHostState() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(63, "fuseboxAssertSingleHostState"); + markFlagAsAccessed(64, "fuseboxAssertSingleHostState"); flagValue = currentProvider_->fuseboxAssertSingleHostState(); fuseboxAssertSingleHostState_ = flagValue; @@ -1190,7 +1208,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(64, "fuseboxEnabledRelease"); + markFlagAsAccessed(65, "fuseboxEnabledRelease"); flagValue = currentProvider_->fuseboxEnabledRelease(); fuseboxEnabledRelease_ = flagValue; @@ -1208,7 +1226,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxFrameRecordingEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(65, "fuseboxFrameRecordingEnabled"); + markFlagAsAccessed(66, "fuseboxFrameRecordingEnabled"); flagValue = currentProvider_->fuseboxFrameRecordingEnabled(); fuseboxFrameRecordingEnabled_ = flagValue; @@ -1226,7 +1244,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxScreenshotCaptureEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(66, "fuseboxScreenshotCaptureEnabled"); + markFlagAsAccessed(67, "fuseboxScreenshotCaptureEnabled"); flagValue = currentProvider_->fuseboxScreenshotCaptureEnabled(); fuseboxScreenshotCaptureEnabled_ = flagValue; @@ -1244,7 +1262,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxWebSocketEventsEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(67, "fuseboxWebSocketEventsEnabled"); + markFlagAsAccessed(68, "fuseboxWebSocketEventsEnabled"); flagValue = currentProvider_->fuseboxWebSocketEventsEnabled(); fuseboxWebSocketEventsEnabled_ = flagValue; @@ -1262,7 +1280,7 @@ bool ReactNativeFeatureFlagsAccessor::keepTimersRunningWhileHostPausedAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(68, "keepTimersRunningWhileHostPausedAndroid"); + markFlagAsAccessed(69, "keepTimersRunningWhileHostPausedAndroid"); flagValue = currentProvider_->keepTimersRunningWhileHostPausedAndroid(); keepTimersRunningWhileHostPausedAndroid_ = flagValue; @@ -1280,7 +1298,7 @@ bool ReactNativeFeatureFlagsAccessor::optimizedAnimatedPropUpdates() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(69, "optimizedAnimatedPropUpdates"); + markFlagAsAccessed(70, "optimizedAnimatedPropUpdates"); flagValue = currentProvider_->optimizedAnimatedPropUpdates(); optimizedAnimatedPropUpdates_ = flagValue; @@ -1298,7 +1316,7 @@ bool ReactNativeFeatureFlagsAccessor::overrideBySynchronousMountPropsAtMountingA // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(70, "overrideBySynchronousMountPropsAtMountingAndroid"); + markFlagAsAccessed(71, "overrideBySynchronousMountPropsAtMountingAndroid"); flagValue = currentProvider_->overrideBySynchronousMountPropsAtMountingAndroid(); overrideBySynchronousMountPropsAtMountingAndroid_ = flagValue; @@ -1316,7 +1334,7 @@ bool ReactNativeFeatureFlagsAccessor::perfIssuesEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(71, "perfIssuesEnabled"); + markFlagAsAccessed(72, "perfIssuesEnabled"); flagValue = currentProvider_->perfIssuesEnabled(); perfIssuesEnabled_ = flagValue; @@ -1334,7 +1352,7 @@ bool ReactNativeFeatureFlagsAccessor::perfMonitorV2Enabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(72, "perfMonitorV2Enabled"); + markFlagAsAccessed(73, "perfMonitorV2Enabled"); flagValue = currentProvider_->perfMonitorV2Enabled(); perfMonitorV2Enabled_ = flagValue; @@ -1352,7 +1370,7 @@ double ReactNativeFeatureFlagsAccessor::preparedTextCacheSize() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(73, "preparedTextCacheSize"); + markFlagAsAccessed(74, "preparedTextCacheSize"); flagValue = currentProvider_->preparedTextCacheSize(); preparedTextCacheSize_ = flagValue; @@ -1370,7 +1388,7 @@ bool ReactNativeFeatureFlagsAccessor::preventShadowTreeCommitExhaustion() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(74, "preventShadowTreeCommitExhaustion"); + markFlagAsAccessed(75, "preventShadowTreeCommitExhaustion"); flagValue = currentProvider_->preventShadowTreeCommitExhaustion(); preventShadowTreeCommitExhaustion_ = flagValue; @@ -1388,7 +1406,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2Android() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(75, "redBoxV2Android"); + markFlagAsAccessed(76, "redBoxV2Android"); flagValue = currentProvider_->redBoxV2Android(); redBoxV2Android_ = flagValue; @@ -1406,7 +1424,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2IOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(76, "redBoxV2IOS"); + markFlagAsAccessed(77, "redBoxV2IOS"); flagValue = currentProvider_->redBoxV2IOS(); redBoxV2IOS_ = flagValue; @@ -1424,7 +1442,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldPressibilityUseW3CPointerEventsForHo // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(77, "shouldPressibilityUseW3CPointerEventsForHover"); + markFlagAsAccessed(78, "shouldPressibilityUseW3CPointerEventsForHover"); flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover(); shouldPressibilityUseW3CPointerEventsForHover_ = flagValue; @@ -1442,7 +1460,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldTriggerResponderTransferOnScrollAndr // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(78, "shouldTriggerResponderTransferOnScrollAndroid"); + markFlagAsAccessed(79, "shouldTriggerResponderTransferOnScrollAndroid"); flagValue = currentProvider_->shouldTriggerResponderTransferOnScrollAndroid(); shouldTriggerResponderTransferOnScrollAndroid_ = flagValue; @@ -1460,7 +1478,7 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(79, "skipActivityIdentityAssertionOnHostPause"); + markFlagAsAccessed(80, "skipActivityIdentityAssertionOnHostPause"); flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause(); skipActivityIdentityAssertionOnHostPause_ = flagValue; @@ -1478,7 +1496,7 @@ bool ReactNativeFeatureFlagsAccessor::skipUnchangedChildrenInDifferentiator() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "skipUnchangedChildrenInDifferentiator"); + markFlagAsAccessed(81, "skipUnchangedChildrenInDifferentiator"); flagValue = currentProvider_->skipUnchangedChildrenInDifferentiator(); skipUnchangedChildrenInDifferentiator_ = flagValue; @@ -1496,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::syncAndroidClipBoundsWithOverflow() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "syncAndroidClipBoundsWithOverflow"); + markFlagAsAccessed(82, "syncAndroidClipBoundsWithOverflow"); flagValue = currentProvider_->syncAndroidClipBoundsWithOverflow(); syncAndroidClipBoundsWithOverflow_ = flagValue; @@ -1514,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(83, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1532,7 +1550,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(84, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1550,7 +1568,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(85, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1568,7 +1586,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(85, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(86, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1586,7 +1604,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(86, "useFabricInterop"); + markFlagAsAccessed(87, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1604,7 +1622,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(87, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(88, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1622,7 +1640,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(88, "useNestedScrollViewAndroid"); + markFlagAsAccessed(89, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1640,7 +1658,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(89, "useSharedAnimatedBackend"); + markFlagAsAccessed(90, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1658,7 +1676,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(90, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(91, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1676,7 +1694,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(91, "useTurboModuleInterop"); + markFlagAsAccessed(92, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1694,7 +1712,7 @@ bool ReactNativeFeatureFlagsAccessor::useWindowDensityForDisplayMetricsAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(92, "useWindowDensityForDisplayMetricsAndroid"); + markFlagAsAccessed(93, "useWindowDensityForDisplayMetricsAndroid"); flagValue = currentProvider_->useWindowDensityForDisplayMetricsAndroid(); useWindowDensityForDisplayMetricsAndroid_ = flagValue; @@ -1712,7 +1730,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(93, "viewCullingOutsetRatio"); + markFlagAsAccessed(94, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1730,7 +1748,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(94, "viewTransitionEnabled"); + markFlagAsAccessed(95, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1748,7 +1766,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionUseHardwareBitmapAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(95, "viewTransitionUseHardwareBitmapAndroid"); + markFlagAsAccessed(96, "viewTransitionUseHardwareBitmapAndroid"); flagValue = currentProvider_->viewTransitionUseHardwareBitmapAndroid(); viewTransitionUseHardwareBitmapAndroid_ = flagValue; @@ -1766,7 +1784,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(96, "virtualViewPrerenderRatio"); + markFlagAsAccessed(97, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 7a9bc59bc57..29aed2380cb 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<17f41335dce7e5b45e7af2878c47d69b>> + * @generated SignedSource<<2d2cbf8b524ee0a120a88f070c09f96f>> */ /** @@ -58,6 +58,7 @@ class ReactNativeFeatureFlagsAccessor { bool enableDestroyShadowTreeRevisionAsync(); bool enableDoubleMeasurementFixAndroid(); bool enableEagerRootViewAttachment(); + bool enableEllipsizedTextTrimmingAndroid(); bool enableExclusivePropsUpdateAndroid(); bool enableFabricCommitBranching(); bool enableFabricCommitBranchingMergeOnMainThread(); @@ -142,7 +143,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 97> accessedFeatureFlags_; + std::array, 98> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -168,6 +169,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> enableDestroyShadowTreeRevisionAsync_; std::atomic> enableDoubleMeasurementFixAndroid_; std::atomic> enableEagerRootViewAttachment_; + std::atomic> enableEllipsizedTextTrimmingAndroid_; std::atomic> enableExclusivePropsUpdateAndroid_; std::atomic> enableFabricCommitBranching_; std::atomic> enableFabricCommitBranchingMergeOnMainThread_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 600aab1882c..540d221df2f 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<35e8175277baa31d135d802ef5a5e6f1>> + * @generated SignedSource<> */ /** @@ -125,6 +125,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool enableEllipsizedTextTrimmingAndroid() override { + return false; + } + bool enableExclusivePropsUpdateAndroid() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 88878807cfe..3459e930955 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<6f8a48ccddbba13d212565740f017f24>> */ /** @@ -263,6 +263,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::enableEagerRootViewAttachment(); } + bool enableEllipsizedTextTrimmingAndroid() override { + auto value = values_["enableEllipsizedTextTrimmingAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableEllipsizedTextTrimmingAndroid(); + } + bool enableExclusivePropsUpdateAndroid() override { auto value = values_["enableExclusivePropsUpdateAndroid"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 66492228700..aea7d13bf94 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<7413e958441d0502dc1266ff87e9da85>> + * @generated SignedSource<> */ /** @@ -51,6 +51,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool enableDestroyShadowTreeRevisionAsync() = 0; virtual bool enableDoubleMeasurementFixAndroid() = 0; virtual bool enableEagerRootViewAttachment() = 0; + virtual bool enableEllipsizedTextTrimmingAndroid() = 0; virtual bool enableExclusivePropsUpdateAndroid() = 0; virtual bool enableFabricCommitBranching() = 0; virtual bool enableFabricCommitBranchingMergeOnMainThread() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index ca8a80f1adb..dfbfaa81dda 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -164,6 +164,11 @@ bool NativeReactNativeFeatureFlags::enableEagerRootViewAttachment( return ReactNativeFeatureFlags::enableEagerRootViewAttachment(); } +bool NativeReactNativeFeatureFlags::enableEllipsizedTextTrimmingAndroid( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::enableEllipsizedTextTrimmingAndroid(); +} + bool NativeReactNativeFeatureFlags::enableExclusivePropsUpdateAndroid( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::enableExclusivePropsUpdateAndroid(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 7413766e285..59116b8ada0 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<90f271ea47f91e6a84979209b7b3d816>> + * @generated SignedSource<<2827752b1ae114bb5626dec944437ccf>> */ /** @@ -86,6 +86,8 @@ class NativeReactNativeFeatureFlags bool enableEagerRootViewAttachment(jsi::Runtime& runtime); + bool enableEllipsizedTextTrimmingAndroid(jsi::Runtime& runtime); + bool enableExclusivePropsUpdateAndroid(jsi::Runtime& runtime); bool enableFabricCommitBranching(jsi::Runtime& runtime); diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 612c196ff22..bb9012396b8 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -311,6 +311,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + enableEllipsizedTextTrimmingAndroid: { + defaultValue: false, + metadata: { + dateAdded: '2026-10-29', + description: + 'Draws ellipsized text on Android without the text hidden after the ellipsis, so that it is not shaped again on every draw.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, enableExclusivePropsUpdateAndroid: { dangerouslyExposeInPublicCppHeaders: true, defaultValue: false, diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index e5a8120cff6..2d3a41b0efe 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<674f975884ae746e067e614523f2bfc3>> + * @generated SignedSource<<39f76cb701cafc08a547b1f222ab9889>> * @flow strict * @noformat */ @@ -73,6 +73,7 @@ export type ReactNativeFeatureFlags = Readonly<{ enableDestroyShadowTreeRevisionAsync: Getter, enableDoubleMeasurementFixAndroid: Getter, enableEagerRootViewAttachment: Getter, + enableEllipsizedTextTrimmingAndroid: Getter, enableExclusivePropsUpdateAndroid: Getter, enableFabricCommitBranching: Getter, enableFabricCommitBranchingMergeOnMainThread: Getter, @@ -318,6 +319,10 @@ export const enableDoubleMeasurementFixAndroid: Getter = createNativeFl * Feature flag to configure eager attachment of the root view/initialisation of the JS code. */ export const enableEagerRootViewAttachment: Getter = createNativeFlagGetter('enableEagerRootViewAttachment', false); +/** + * Draws ellipsized text on Android without the text hidden after the ellipsis, so that it is not shaped again on every draw. + */ +export const enableEllipsizedTextTrimmingAndroid: Getter = createNativeFlagGetter('enableEllipsizedTextTrimmingAndroid', false); /** * When enabled, Android will disable Props 1.5 raw value merging when Props 2.0 is available. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 6fc752bba2a..174f9223c6e 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2eb3f4a3dca013c51ad2913944855010>> + * @generated SignedSource<<0725b0909ae9dfb60e8ebe1ee1097851>> * @flow strict * @noformat */ @@ -49,6 +49,7 @@ export interface Spec extends TurboModule { readonly enableDestroyShadowTreeRevisionAsync?: () => boolean; readonly enableDoubleMeasurementFixAndroid?: () => boolean; readonly enableEagerRootViewAttachment?: () => boolean; + readonly enableEllipsizedTextTrimmingAndroid?: () => boolean; readonly enableExclusivePropsUpdateAndroid?: () => boolean; readonly enableFabricCommitBranching?: () => boolean; readonly enableFabricCommitBranchingMergeOnMainThread?: () => boolean;