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 7fcdaaeec16d..ca80f04261ef 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<<8224c3abed0fa461f272b4bfaa9ec698>> */ /** @@ -396,6 +396,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean = accessor.fixMappingOfEventPrioritiesBetweenFabricAndReact() + /** + * Grows lines of text with `lineHeight` to fit taller inline views instead of letting them overflow, keeps lines after an inline view at `lineHeight` on Android, and aligns inline views with the text baseline on iOS. + */ + @JvmStatic + public fun fixTextInlineViewLineHeight(): Boolean = accessor.fixTextInlineViewLineHeight() + /** * Re-measures text and re-sends padding and overflow insets to mounted views when the pixel density changes, so text and padded views do not keep sizes from the old density. */ 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 6e0da4d9d0e4..f946f5eae20f 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<<1b9a676a7359de07c9660be087c262dc>> */ /** @@ -81,6 +81,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var enableVirtualViewContainerStateExperimentalCache: Boolean? = null private var fixBorderlessRippleAndroidCache: Boolean? = null private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null + private var fixTextInlineViewLineHeightCache: Boolean? = null private var fixTextLayoutOnPixelDensityChangeCache: Boolean? = null private var fixYogaFlexBasisFitContentInMainAxisCache: Boolean? = null private var fuseboxAssertSingleHostStateCache: Boolean? = null @@ -667,6 +668,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun fixTextInlineViewLineHeight(): Boolean { + var cached = fixTextInlineViewLineHeightCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.fixTextInlineViewLineHeight() + fixTextInlineViewLineHeightCache = cached + } + return cached + } + override fun fixTextLayoutOnPixelDensityChange(): Boolean { var cached = fixTextLayoutOnPixelDensityChangeCache 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 2268d03f3674..dd8cb8714830 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<> */ /** @@ -150,6 +150,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean + @DoNotStrip @JvmStatic public external fun fixTextInlineViewLineHeight(): Boolean + @DoNotStrip @JvmStatic public external fun fixTextLayoutOnPixelDensityChange(): Boolean @DoNotStrip @JvmStatic public external fun fixYogaFlexBasisFitContentInMainAxis(): 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 defafe46f6b4..969389fd0f0e 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<<63ab6b4c29d6a2cf2d4bab0f5677d901>> */ /** @@ -145,6 +145,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean = false + override fun fixTextInlineViewLineHeight(): Boolean = false + override fun fixTextLayoutOnPixelDensityChange(): Boolean = false override fun fixYogaFlexBasisFitContentInMainAxis(): 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 53af4b4edab0..110e5f910745 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<<974bc6b82dc2de7c83c381ad960cbdfc>> */ /** @@ -85,6 +85,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var enableVirtualViewContainerStateExperimentalCache: Boolean? = null private var fixBorderlessRippleAndroidCache: Boolean? = null private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null + private var fixTextInlineViewLineHeightCache: Boolean? = null private var fixTextLayoutOnPixelDensityChangeCache: Boolean? = null private var fixYogaFlexBasisFitContentInMainAxisCache: Boolean? = null private var fuseboxAssertSingleHostStateCache: Boolean? = null @@ -732,6 +733,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun fixTextInlineViewLineHeight(): Boolean { + var cached = fixTextInlineViewLineHeightCache + if (cached == null) { + cached = currentProvider.fixTextInlineViewLineHeight() + accessedFeatureFlags.add("fixTextInlineViewLineHeight") + fixTextInlineViewLineHeightCache = cached + } + return cached + } + override fun fixTextLayoutOnPixelDensityChange(): Boolean { var cached = fixTextLayoutOnPixelDensityChangeCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsOverrides_RNOSS_Canary_Android.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsOverrides_RNOSS_Canary_Android.kt index 91f5e33fdd1b..a1a1492e4398 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsOverrides_RNOSS_Canary_Android.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsOverrides_RNOSS_Canary_Android.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<<4f36eb12d5d8cd8a955b46fbb164bc25>> */ /** @@ -35,6 +35,8 @@ public open class ReactNativeFeatureFlagsOverrides_RNOSS_Canary_Android : ReactN override fun fixBorderlessRippleAndroid(): Boolean = true + override fun fixTextInlineViewLineHeight(): Boolean = true + override fun fuseboxFrameRecordingEnabled(): Boolean = true override fun fuseboxWebSocketEventsEnabled(): Boolean = true 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 8fa2f4269464..310889832427 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<<65c68d67b088b44cde90cebd7bda9e63>> */ /** @@ -145,6 +145,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean + @DoNotStrip public fun fixTextInlineViewLineHeight(): Boolean + @DoNotStrip public fun fixTextLayoutOnPixelDensityChange(): Boolean @DoNotStrip public fun fixYogaFlexBasisFitContentInMainAxis(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CustomLineHeightSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CustomLineHeightSpan.kt index 5d30f853c3b2..989d78618475 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CustomLineHeightSpan.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CustomLineHeightSpan.kt @@ -8,7 +8,11 @@ package com.facebook.react.views.text.internal.span import android.graphics.Paint.FontMetricsInt +import android.text.Spanned +import android.text.TextPaint import android.text.style.LineHeightSpan +import android.text.style.MetricAffectingSpan +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import kotlin.math.ceil import kotlin.math.floor @@ -17,7 +21,7 @@ import kotlin.math.floor * LineHeightSpan.Standard which only effects space between the baselines of adjacent line boxes * (does not impact space before the first line or after the last). */ -internal class CustomLineHeightSpan(height: Float) : LineHeightSpan, ReactSpan { +internal class CustomLineHeightSpan(height: Float) : LineHeightSpan.WithDensity, ReactSpan { val lineHeight: Int = ceil(height.toDouble()).toInt() override fun chooseHeight( @@ -53,4 +57,95 @@ internal class CustomLineHeightSpan(height: Float) : LineHeightSpan, ReactSpan { fm.bottom = fm.descent } } + + override fun chooseHeight( + text: CharSequence, + start: Int, + end: Int, + spanstartv: Int, + v: Int, + fm: FontMetricsInt, + paint: TextPaint, + ) { + val spanned = text as? Spanned + if ( + !ReactNativeFeatureFlags.fixTextInlineViewLineHeight() || + spanned == null || + !containsInlineView(spanned) + ) { + chooseHeight(text, start, end, spanstartv, v, fm) + return + } + + // If text contains an inline view anywhere, remeasure fm before calling chooseHeight() to avoid + // clipping the view. StaticLayout seeds the next line's fm with this line's if a text run + // wraps, so remeasurement is needed even if an inline view doesn't exist on the current line. + val inlineViewHeight = maxInlineViewHeight(spanned, start, end) + measureTextRuns(spanned, start, end, paint, fm) + chooseHeight(text, start, end, spanstartv, v, fm) + if (-fm.ascent < inlineViewHeight) { + fm.ascent = -inlineViewHeight + fm.top = minOf(fm.top, fm.ascent) + } + } + + /** + * Sets [fm] to the combined font metrics of the text in [start, end), excluding inline views. + * + * The line is split into runs at each [MetricAffectingSpan] transition. Each run that is not an + * inline view placeholder is measured with [basePaint] after applying that run's spans (font + * size, typeface, etc.), and [fm] encloses the tallest ascent and deepest descent across runs. If + * the line has no text runs (only inline views), [fm] falls back to the metrics of [basePaint], + * matching the strut of a CSS line box. + * + * Only the primary font of each run is measured, so unlike StaticLayout, glyphs drawn with a + * fallback font (such as emoji) do not contribute to the metrics. + */ + private fun measureTextRuns( + text: Spanned, + start: Int, + end: Int, + basePaint: TextPaint, + fm: FontMetricsInt, + ) { + val runPaint = TextPaint() + val runFm = FontMetricsInt() + fm.top = 0 + fm.ascent = 0 + fm.descent = 0 + fm.bottom = 0 + var hasTextRun = false + var runStart = start + while (runStart < end) { + val runEnd = text.nextSpanTransition(runStart, end, MetricAffectingSpan::class.java) + if (text.getSpans(runStart, runEnd, TextInlineViewPlaceholderSpan::class.java).isEmpty()) { + hasTextRun = true + runPaint.set(basePaint) + text.getSpans(runStart, runEnd, MetricAffectingSpan::class.java).forEach { + it.updateMeasureState(runPaint) + } + runPaint.getFontMetricsInt(runFm) + fm.top = minOf(fm.top, runFm.top) + fm.ascent = minOf(fm.ascent, runFm.ascent) + fm.descent = maxOf(fm.descent, runFm.descent) + fm.bottom = maxOf(fm.bottom, runFm.bottom) + } + runStart = runEnd + } + if (!hasTextRun) { + basePaint.getFontMetricsInt(fm) + } + } + + /** Returns whether [text] contains an inline view anywhere, not just on the current line. */ + private fun containsInlineView(text: Spanned): Boolean { + val firstTransition = + text.nextSpanTransition(0, text.length, TextInlineViewPlaceholderSpan::class.java) + return firstTransition < text.length + } + + /** Returns the height of the tallest inline view in [start, end), or 0 if there are none. */ + private fun maxInlineViewHeight(text: Spanned, start: Int, end: Int): Int = + text.getSpans(start, end, TextInlineViewPlaceholderSpan::class.java).maxOfOrNull { it.height } + ?: 0 } 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 23fd0978a766..23776f5be561 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<> */ /** @@ -405,6 +405,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool fixTextInlineViewLineHeight() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("fixTextInlineViewLineHeight"); + return method(javaProvider_); + } + bool fixTextLayoutOnPixelDensityChange() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("fixTextLayoutOnPixelDensityChange"); @@ -930,6 +936,11 @@ bool JReactNativeFeatureFlagsCxxInterop::fixMappingOfEventPrioritiesBetweenFabri return ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact(); } +bool JReactNativeFeatureFlagsCxxInterop::fixTextInlineViewLineHeight( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::fixTextInlineViewLineHeight(); +} + bool JReactNativeFeatureFlagsCxxInterop::fixTextLayoutOnPixelDensityChange( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange(); @@ -1324,6 +1335,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "fixMappingOfEventPrioritiesBetweenFabricAndReact", JReactNativeFeatureFlagsCxxInterop::fixMappingOfEventPrioritiesBetweenFabricAndReact), + makeNativeMethod( + "fixTextInlineViewLineHeight", + JReactNativeFeatureFlagsCxxInterop::fixTextInlineViewLineHeight), makeNativeMethod( "fixTextLayoutOnPixelDensityChange", JReactNativeFeatureFlagsCxxInterop::fixTextLayoutOnPixelDensityChange), 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 ae774f80c82b..de722e0f0b5b 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<<7c754a14e96a400f346076811c13cecf>> */ /** @@ -213,6 +213,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool fixMappingOfEventPrioritiesBetweenFabricAndReact( facebook::jni::alias_ref); + static bool fixTextInlineViewLineHeight( + facebook::jni::alias_ref); + static bool fixTextLayoutOnPixelDensityChange( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index 4b24a9161d64..aa6cd66a15ee 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<> + * @generated SignedSource<> */ /** @@ -270,6 +270,10 @@ bool ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact() return getAccessor().fixMappingOfEventPrioritiesBetweenFabricAndReact(); } +bool ReactNativeFeatureFlags::fixTextInlineViewLineHeight() { + return getAccessor().fixTextInlineViewLineHeight(); +} + bool ReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange() { return getAccessor().fixTextLayoutOnPixelDensityChange(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 0e5b3f2013fb..240bb068d58f 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<<242f143d34bd97101cb0d71075cfcd9e>> */ /** @@ -346,6 +346,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool fixMappingOfEventPrioritiesBetweenFabricAndReact(); + /** + * Grows lines of text with `lineHeight` to fit taller inline views instead of letting them overflow, keeps lines after an inline view at `lineHeight` on Android, and aligns inline views with the text baseline on iOS. + */ + RN_EXPORT static bool fixTextInlineViewLineHeight(); + /** * Re-measures text and re-sends padding and overflow insets to mounted views when the pixel density changes, so text and padded views do not keep sizes from the old density. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 7640cea6418a..5ecaf302058c 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<<569e9a81d879fd95739d2ba67d944266>> */ /** @@ -1127,6 +1127,24 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::fixTextInlineViewLineHeight() { + auto flagValue = fixTextInlineViewLineHeight_.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(61, "fixTextInlineViewLineHeight"); + + flagValue = currentProvider_->fixTextInlineViewLineHeight(); + fixTextInlineViewLineHeight_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::fixTextLayoutOnPixelDensityChange() { auto flagValue = fixTextLayoutOnPixelDensityChange_.load(); @@ -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 7a9bc59bc57e..12f9ad72765d 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<<718db75add9d3b4d921375e2d6f6dfe0>> */ /** @@ -95,6 +95,7 @@ class ReactNativeFeatureFlagsAccessor { bool enableVirtualViewContainerStateExperimental(); bool fixBorderlessRippleAndroid(); bool fixMappingOfEventPrioritiesBetweenFabricAndReact(); + bool fixTextInlineViewLineHeight(); bool fixTextLayoutOnPixelDensityChange(); bool fixYogaFlexBasisFitContentInMainAxis(); bool fuseboxAssertSingleHostState(); @@ -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_; @@ -205,6 +206,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> enableVirtualViewContainerStateExperimental_; std::atomic> fixBorderlessRippleAndroid_; std::atomic> fixMappingOfEventPrioritiesBetweenFabricAndReact_; + std::atomic> fixTextInlineViewLineHeight_; std::atomic> fixTextLayoutOnPixelDensityChange_; std::atomic> fixYogaFlexBasisFitContentInMainAxis_; std::atomic> fuseboxAssertSingleHostState_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 600aab1882cc..74462a3cad9d 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<<05b0dbc6197f4f256a93ab0102b8251f>> */ /** @@ -273,6 +273,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool fixTextInlineViewLineHeight() override { + return false; + } + bool fixTextLayoutOnPixelDensityChange() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 88878807cfec..6b2a67634703 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<> */ /** @@ -596,6 +596,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::fixMappingOfEventPrioritiesBetweenFabricAndReact(); } + bool fixTextInlineViewLineHeight() override { + auto value = values_["fixTextInlineViewLineHeight"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::fixTextInlineViewLineHeight(); + } + bool fixTextLayoutOnPixelDensityChange() override { auto value = values_["fixTextLayoutOnPixelDensityChange"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsOverridesOSSCanary.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsOverridesOSSCanary.h index 4bf6a6363c3b..e08749b19ed9 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsOverridesOSSCanary.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsOverridesOSSCanary.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<<4b66991416806affd46aab79e36c4a45>> + * @generated SignedSource<<25ed26f9f7db8a224d71080a9c1d4d06>> */ /** @@ -53,6 +53,10 @@ class ReactNativeFeatureFlagsOverridesOSSCanary : public ReactNativeFeatureFlags return true; } + bool fixTextInlineViewLineHeight() override { + return true; + } + bool fuseboxFrameRecordingEnabled() override { return true; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 664922287009..faeb01ea4e8b 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<<6ad742054f19a28ab1de89e786874a25>> */ /** @@ -88,6 +88,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool enableVirtualViewContainerStateExperimental() = 0; virtual bool fixBorderlessRippleAndroid() = 0; virtual bool fixMappingOfEventPrioritiesBetweenFabricAndReact() = 0; + virtual bool fixTextInlineViewLineHeight() = 0; virtual bool fixTextLayoutOnPixelDensityChange() = 0; virtual bool fixYogaFlexBasisFitContentInMainAxis() = 0; virtual bool fuseboxAssertSingleHostState() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index ca8a80f1adbf..1fe712b139e8 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<> */ /** @@ -349,6 +349,11 @@ bool NativeReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndR return ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact(); } +bool NativeReactNativeFeatureFlags::fixTextInlineViewLineHeight( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::fixTextInlineViewLineHeight(); +} + bool NativeReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 7413766e285a..51df8df52c62 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<> */ /** @@ -160,6 +160,8 @@ class NativeReactNativeFeatureFlags bool fixMappingOfEventPrioritiesBetweenFabricAndReact(jsi::Runtime& runtime); + bool fixTextInlineViewLineHeight(jsi::Runtime& runtime); + bool fixTextLayoutOnPixelDensityChange(jsi::Runtime& runtime); bool fixYogaFlexBasisFitContentInMainAxis(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm index 3e93b60e64be..a2711d53fb4c 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm @@ -21,6 +21,9 @@ using namespace facebook::react; +@interface RCTTextLayoutManager () +@end + @implementation RCTTextLayoutManager { SimpleThreadSafeCache, 256> _cache; } @@ -445,6 +448,10 @@ - (NSTextStorage *)_textStorageAndLayoutManagerWithAttributesString:(NSAttribute NSLayoutManager *layoutManager = [NSLayoutManager new]; layoutManager.usesFontLeading = NO; + if (ReactNativeFeatureFlags::fixTextInlineViewLineHeight() && + [attributedString containsAttachmentsInRange:NSMakeRange(0, attributedString.length)]) { + layoutManager.delegate = self; + } [layoutManager addTextContainer:textContainer]; NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString]; @@ -467,6 +474,47 @@ - (NSTextStorage *)_textStorageAndLayoutManagerWithAttributesString:(NSAttribute return textStorage; } +#pragma mark - NSLayoutManagerDelegate + +- (BOOL)layoutManager:(NSLayoutManager *)layoutManager + shouldSetLineFragmentRect:(inout CGRect *)lineFragmentRect + lineFragmentUsedRect:(inout CGRect *)lineFragmentUsedRect + baselineOffset:(inout CGFloat *)baselineOffset + inTextContainer:(NSTextContainer *)textContainer + forGlyphRange:(NSRange)glyphRange +{ + NSTextStorage *textStorage = layoutManager.textStorage; + NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:nil]; + + // Tallest space any attachment on this line needs above the baseline, counting the NSBaselineOffset lift that + // RCTApplyBaselineOffset gives the text. + __block CGFloat requiredAscent = 0; + [textStorage enumerateAttribute:NSAttachmentAttributeName + inRange:characterRange + options:0 + usingBlock:^(NSTextAttachment *attachment, NSRange range, __unused BOOL *stop) { + if (attachment == nil) { + return; + } + CGFloat baselineShift = [[textStorage attribute:NSBaselineOffsetAttributeName + atIndex:range.location + effectiveRange:nil] doubleValue]; + requiredAscent = MAX(attachment.bounds.size.height + baselineShift, requiredAscent); + }]; + + // maximumLineHeight (from lineHeight) caps the line even when an attachment is taller. + // Inline views aren't subject to lineHeight, so add the missing height above the baseline. + CGFloat missingHeight = requiredAscent - *baselineOffset; + if (missingHeight <= 0) { + return NO; + } + + lineFragmentRect->size.height += missingHeight; + lineFragmentUsedRect->size.height += missingHeight; + *baselineOffset += missingHeight; + return YES; +} + - (std::shared_ptr)getEventEmitterWithAttributeString:(const AttributedString &)attributedString paragraphAttributes:(ParagraphAttributes)paragraphAttributes frame:(CGRect)frame @@ -646,10 +694,17 @@ - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage UIFont *font = [[textStorage attributedSubstringFromRange:range] attribute:NSFontAttributeName atIndex:0 effectiveRange:nil]; + // The text is drawn lifted by NSBaselineOffset (see RCTApplyBaselineOffset), so lift the + // attachment by the same amount to keep it on the text's baseline. + CGFloat baselineShift = ReactNativeFeatureFlags::fixTextInlineViewLineHeight() + ? [[textStorage attribute:NSBaselineOffsetAttributeName atIndex:range.location + effectiveRange:nil] doubleValue] + : 0; frame = { .origin = {glyphRect.origin.x, - glyphRect.origin.y + glyphRect.size.height - attachmentSize.height + font.descender}, + glyphRect.origin.y + glyphRect.size.height - attachmentSize.height + font.descender - + baselineShift}, .size = attachmentSize}; auto rect = facebook::react::Rect{ diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 612c196ff22d..2f02713aa70f 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -707,6 +707,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + fixTextInlineViewLineHeight: { + defaultValue: false, + metadata: { + dateAdded: '2026-10-20', + description: + 'Grows lines of text with `lineHeight` to fit taller inline views instead of letting them overflow, keeps lines after an inline view at `lineHeight` on Android, and aligns inline views with the text baseline on iOS.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'canary', + }, fixTextLayoutOnPixelDensityChange: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index e5a8120cff62..396dc8c573c8 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<<5d983c219076f0cdd2b96a39d5edb3d8>> * @flow strict * @noformat */ @@ -110,6 +110,7 @@ export type ReactNativeFeatureFlags = Readonly<{ enableVirtualViewContainerStateExperimental: Getter, fixBorderlessRippleAndroid: Getter, fixMappingOfEventPrioritiesBetweenFabricAndReact: Getter, + fixTextInlineViewLineHeight: Getter, fixTextLayoutOnPixelDensityChange: Getter, fixYogaFlexBasisFitContentInMainAxis: Getter, fuseboxAssertSingleHostState: Getter, @@ -466,6 +467,10 @@ export const fixBorderlessRippleAndroid: Getter = createNativeFlagGette * Uses the default event priority instead of the discreet event priority by default when dispatching events from Fabric to React. */ export const fixMappingOfEventPrioritiesBetweenFabricAndReact: Getter = createNativeFlagGetter('fixMappingOfEventPrioritiesBetweenFabricAndReact', false); +/** + * Grows lines of text with `lineHeight` to fit taller inline views instead of letting them overflow, keeps lines after an inline view at `lineHeight` on Android, and aligns inline views with the text baseline on iOS. + */ +export const fixTextInlineViewLineHeight: Getter = createNativeFlagGetter('fixTextInlineViewLineHeight', false); /** * Re-measures text and re-sends padding and overflow insets to mounted views when the pixel density changes, so text and padded views do not keep sizes from the old density. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 6fc752bba2a9..ffd562adecb4 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<<24c8076b2d995499debb756d1d152d67>> * @flow strict * @noformat */ @@ -86,6 +86,7 @@ export interface Spec extends TurboModule { readonly enableVirtualViewContainerStateExperimental?: () => boolean; readonly fixBorderlessRippleAndroid?: () => boolean; readonly fixMappingOfEventPrioritiesBetweenFabricAndReact?: () => boolean; + readonly fixTextInlineViewLineHeight?: () => boolean; readonly fixTextLayoutOnPixelDensityChange?: () => boolean; readonly fixYogaFlexBasisFitContentInMainAxis?: () => boolean; readonly fuseboxAssertSingleHostState?: () => boolean; diff --git a/packages/rn-tester/js/components/TextInlineView.js b/packages/rn-tester/js/components/TextInlineView.js index 4f87cc5df978..5ea2c8a8c01e 100644 --- a/packages/rn-tester/js/components/TextInlineView.js +++ b/packages/rn-tester/js/components/TextInlineView.js @@ -199,6 +199,64 @@ function ChangeInnerViewSize(): React.Node { ); } +component ChangeLineHeight() { + return ( + + + + + + ); +} + +component InlineImageLineHeightCase( + label: string, + size: number, + lineHeight?: number, +) { + return ( + + {label} + + + This text{' '} + {' '} + contains multiple inline blue views. The inline views should be + contained within the container and should not overflow on any other + lines.{' '} + {' '} + The line spacing between every line should be consistent as well.{' '} + + + + + ); +} + module.exports = { Basic, NestedTexts, @@ -206,4 +264,5 @@ module.exports = { ChangeImageSize, ChangeViewSize, ChangeInnerViewSize, + ChangeLineHeight, }; diff --git a/packages/rn-tester/js/examples/Text/TextExample.android.js b/packages/rn-tester/js/examples/Text/TextExample.android.js index aae2faacf9a0..f3994c72a8f1 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.android.js +++ b/packages/rn-tester/js/examples/Text/TextExample.android.js @@ -1767,6 +1767,13 @@ const examples = [ return ; }, }, + { + title: 'Inline views with line height', + name: 'inlineViewsLineHeight', + render(): React.Node { + return ; + }, + }, { title: 'Inline views with multiple nested texts', name: 'inlineViewsMultiple', diff --git a/packages/rn-tester/js/examples/Text/TextExample.ios.js b/packages/rn-tester/js/examples/Text/TextExample.ios.js index ff5ff29a097f..f761a592ac06 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.ios.js +++ b/packages/rn-tester/js/examples/Text/TextExample.ios.js @@ -1310,6 +1310,13 @@ const examples = [ title: 'Inline views', render: (): React.Node => , }, + { + title: 'Inline views with line height', + name: 'inlineViewsLineHeight', + render(): React.Node { + return ; + }, + }, { title: 'Inline image/view clipped by ', render: (): React.Node => ,