From 032ee4537bef36098e40fcfe0e528b8ac0bf8f0c Mon Sep 17 00:00:00 2001 From: Jason Zheng Date: Thu, 22 Oct 2026 16:43:44 -0700 Subject: [PATCH 1/2] Add inline view with lineHeight example to TextInlineView Summary: Add a new RNTester example in TextInlineView that reproduces the issue where setting a line height for Text breaks expected CSS behavior with inline views and images. Changelog: [Internal] Differential Revision: D126542681 --- .../rn-tester/js/components/TextInlineView.js | 59 +++++++++++++++++++ .../js/examples/Text/TextExample.android.js | 7 +++ .../js/examples/Text/TextExample.ios.js | 7 +++ 3 files changed, 73 insertions(+) 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 => , From 78512722c7c65153afded36ea09b3aefef768c3d Mon Sep 17 00:00:00 2001 From: Jason Zheng Date: Mon, 26 Oct 2026 11:54:13 -0700 Subject: [PATCH 2/2] Support inline views in Text with lineHeight (#59420) Summary: Pull Request resolved: https://github.com/react/react-native/pull/59420 Fixes the issue described in [#48727](https://github.com/react/react-native/issues/48727), where adding inline views and images into Text when line height is specified results in the image clipping and overflowing the parent container. ### Root Cause On Android, CustomLineHeightSpan uses the line's merged font metrics `fm` to set the line's ascent/descent so text clamps to `lineHeight`. However, the calculation assumes `fm` only describes text whereas in reality, it also includes the height of inline views/images. As a result, taller images are also clamped to `lineHeight` and overflow. Additionally, because `StaticLayout` seeds each line with the previous line's adjusted font metrics when a text run wraps, the lines after the inline view also inherit its height and get misplaced baselines. The result is that wrapped text following an inline image appear unusually squashed. On iOS, `lineHeight` sets both `minimumLineHeight` and `maximumLineHeight` so TextKit clamps every line to the same height, even for lines with a taller inline view/image. Additionally, `RCTApplyBaselineOffset` lifts the text with `NSBaselineOffset` to center it in the line, but for inline views, this is computed from the unlifted baseline, causing images to sit below the text baseline. ### This PR For Android, modify CustomLineHeightSpan so that if the text includes an inline view, we use `TextPaint` to re-derive `fm` via `measureTextRun()` for every line. This means `fm` only describes text during the `chooseHeight()` calculation, and each line is unaffected by wrapping issues. Then, raise the line's ascent/top to fit the inline view. For iOS, set `RCTTextLayoutManager` as a `NSLayoutManagerDelegate` if an inline view exists so the layout manager calls `layoutManagerShouldSetLineFragmentRect()` which grows the line to match the height of the inline view. Separately, modify the baseline shift so that attachments match the text baseline. All changes are gated behind the `fixTextInlineViewLineHeight` flag. Changelog: [General][Fixed] - Inline views in Text with lineHeight no longer overflow the text Differential Revision: D126795867 --- .../featureflags/ReactNativeFeatureFlags.kt | 8 +- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 ++- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 ++- ...tureFlagsOverrides_RNOSS_Canary_Android.kt | 4 +- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../internal/span/CustomLineHeightSpan.kt | 97 ++++++++++++++++++- .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 ++- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 +- .../ReactNativeFeatureFlagsAccessor.cpp | 92 +++++++++++------- .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 ++- ...eactNativeFeatureFlagsOverridesOSSCanary.h | 6 +- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../textlayoutmanager/RCTTextLayoutManager.mm | 57 ++++++++++- .../ReactNativeFeatureFlags.config.js | 11 +++ .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 24 files changed, 333 insertions(+), 60 deletions(-) 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;