diff --git a/packages/react-native/Libraries/Text/TextProps.js b/packages/react-native/Libraries/Text/TextProps.js index 38e9899f2dfc..c8f28ddc4b49 100644 --- a/packages/react-native/Libraries/Text/TextProps.js +++ b/packages/react-native/Libraries/Text/TextProps.js @@ -122,8 +122,6 @@ export type TextPropsAndroid = { /** * Smallest possible font scale when `adjustsFontSizeToFit` is enabled * (values 0.01-1.0). - * - * @platform ios */ minimumFontScale?: ?number, }; diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 10730dda4a4b..3feb7cea1fe0 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -6044,7 +6044,7 @@ public class com/facebook/react/views/text/ReactTextView : androidx/appcompat/wi public fun setIncludeFontPadding (Z)V public fun setLetterSpacing (F)V public fun setLinkifyMask (I)V - public fun setMinimumFontSize (F)V + public fun setMinimumFontScale (F)V public fun setNumberOfLines (I)V public fun setOverflow (Ljava/lang/String;)V public fun setSpanned (Landroid/text/Spannable;)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index eed89a0aa568..8cd8d4005f91 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -71,7 +71,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie private @Nullable TextUtils.TruncateAt mEllipsizeLocation; private boolean mAdjustsFontSizeToFit; private float mFontSize; - private float mMinimumFontSize; + private float mMinimumFontScale; private float mLetterSpacing; private int mLinkifyMaskType; private boolean mTextIsSelectable; @@ -132,7 +132,7 @@ private void initView() { mShouldAdjustSpannableFontSize = false; mEllipsizeLocation = TextUtils.TruncateAt.END; mFontSize = Float.NaN; - mMinimumFontSize = Float.NaN; + mMinimumFontScale = Float.NaN; mLetterSpacing = 0.f; mOverflow = Overflow.VISIBLE; mSpanned = null; @@ -238,7 +238,7 @@ protected void onDraw(Canvas canvas) { YogaMeasureMode.EXACTLY, getHeight(), YogaMeasureMode.EXACTLY, - mMinimumFontSize, + mMinimumFontScale, mNumberOfLines, getIncludeFontPadding(), getBreakStrategy(), @@ -540,8 +540,8 @@ public void setFontSize(float fontSize) { applyTextAttributes(); } - public void setMinimumFontSize(float minimumFontSize) { - mMinimumFontSize = minimumFontSize; + public void setMinimumFontScale(float minimumFontScale) { + mMinimumFontScale = minimumFontScale; mShouldAdjustSpannableFontSize = true; } @@ -585,9 +585,7 @@ public void setEllipsizeLocation(@Nullable TextUtils.TruncateAt ellipsizeLocatio public void updateView() { @Nullable TextUtils.TruncateAt ellipsizeLocation = - mNumberOfLines == ViewDefaults.NUMBER_OF_LINES || mAdjustsFontSizeToFit - ? null - : mEllipsizeLocation; + mNumberOfLines == ViewDefaults.NUMBER_OF_LINES ? null : mEllipsizeLocation; setEllipsize(ellipsizeLocation); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt index eda2036c2b4d..5c854c5c67a6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt @@ -168,9 +168,9 @@ public constructor( ) view.setSpanned(spanned) - val minimumFontSize: Float = - paragraphAttributes.getDouble(TextLayoutManager.PA_KEY_MINIMUM_FONT_SIZE).toFloat() - view.setMinimumFontSize(minimumFontSize) + val minimumFontScale: Float = + paragraphAttributes.getDouble(TextLayoutManager.PA_KEY_MINIMUM_FONT_SCALE).toFloat() + view.setMinimumFontScale(minimumFontScale) // Clear any stale PreparedLayout from a previous update view.setPreparedLayout(null) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt index 5ea878dcef05..a31a707ab251 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt @@ -93,8 +93,7 @@ internal object TextLayoutManager { const val PA_KEY_ADJUST_FONT_SIZE_TO_FIT: Int = 3 const val PA_KEY_INCLUDE_FONT_PADDING: Int = 4 const val PA_KEY_HYPHENATION_FREQUENCY: Int = 5 - const val PA_KEY_MINIMUM_FONT_SIZE: Int = 6 - const val PA_KEY_MAXIMUM_FONT_SIZE: Int = 7 + const val PA_KEY_MINIMUM_FONT_SCALE: Int = 6 const val PA_KEY_TEXT_ALIGN_VERTICAL: Int = 8 const val PA_KEY_TEXT_WIDTH_MODE: Int = 9 @@ -1046,9 +1045,9 @@ internal object TextLayoutManager { val justificationMode = getTextJustificationMode(alignmentAttr) if (adjustFontSizeToFit) { - val minimumFontSize = - if (paragraphAttributes.contains(PA_KEY_MINIMUM_FONT_SIZE)) - paragraphAttributes.getDouble(PA_KEY_MINIMUM_FONT_SIZE).toFloat() + val minimumFontScale = + if (paragraphAttributes.contains(PA_KEY_MINIMUM_FONT_SCALE)) + paragraphAttributes.getDouble(PA_KEY_MINIMUM_FONT_SCALE).toFloat() else Float.NaN adjustSpannableFontToFit( @@ -1057,7 +1056,7 @@ internal object TextLayoutManager { YogaMeasureMode.EXACTLY, height, heightYogaMeasureMode, - minimumFontSize, + minimumFontScale, maximumNumberOfLines, includeFontPadding, textBreakStrategy, @@ -1209,7 +1208,7 @@ internal object TextLayoutManager { widthYogaMeasureMode: YogaMeasureMode, height: Float, heightYogaMeasureMode: YogaMeasureMode, - minimumFontSizeAttr: Float, + minimumFontScale: Float, maximumNumberOfLines: Int, includeFontPadding: Boolean, textBreakStrategy: Int, @@ -1221,17 +1220,21 @@ internal object TextLayoutManager { var boring = isBoring(text, paint) var layout: Layout - // Minimum font size is 4pts to match the iOS implementation. - val minimumFontSize = - (if (minimumFontSizeAttr.isNaN()) 4.dpToPx() else minimumFontSizeAttr).toInt() - // Find the largest font size used in the spannable to use as a starting point. - var currentFontSize = minimumFontSize + var currentFontSize = 0 val spans = text.getSpans(0, text.length, ReactAbsoluteSizeSpan::class.java) for (span in spans) { currentFontSize = max(currentFontSize, span.size) } + // The smallest font size is the largest font size scaled by minimumFontScale, floored at 4dp + // to match the iOS implementation. + val absoluteMinimumFontSize = 4.dpToPx().toInt() + val minimumFontSize = + if (minimumFontScale.isNaN() || minimumFontScale <= 0f) absoluteMinimumFontSize + else max((minimumFontScale * currentFontSize).toInt(), absoluteMinimumFontSize) + currentFontSize = max(currentFontSize, minimumFontSize) + var intervalStart = minimumFontSize var intervalEnd = currentFontSize var previousFontSize = currentFontSize diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextViewTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextViewTest.kt index 8f07d2159077..5c2eb50719fd 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextViewTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextViewTest.kt @@ -14,6 +14,7 @@ import android.graphics.Color import android.graphics.Paint import android.text.SpannableString import android.text.Spanned +import android.text.TextUtils import android.text.style.ReplacementSpan import android.util.TypedValue import android.view.Gravity @@ -21,8 +22,11 @@ import android.view.View import android.view.ViewGroup import androidx.core.graphics.createBitmap import androidx.core.graphics.get +import com.facebook.react.uimanager.DisplayMetricsHolder import com.facebook.react.views.text.internal.span.ReactAbsoluteSizeSpan import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -32,6 +36,16 @@ import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) class ReactTextViewTest { + @Before + fun setUp() { + DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(RuntimeEnvironment.getApplication()) + } + + @After + fun tearDown() { + DisplayMetricsHolder.setScreenDisplayMetrics(null) + } + @Test fun drawsGlyphInkOutsideLineHeightWhenOverflowIsVisible() { val bitmap = drawReactTextViewWithOverflow(null) @@ -70,7 +84,7 @@ class ReactTextViewTest { ViewGroup.LayoutParams.WRAP_CONTENT, ) view.setTextColor(Color.BLACK) - view.setMinimumFontSize(4f) + view.setMinimumFontScale(0.1f) view.setNumberOfLines(0) view.setAdjustFontSizeToFit(true) view.setSpanned(text) @@ -109,6 +123,18 @@ class ReactTextViewTest { assertThat(view.useBoundsForWidth).isFalse() } + @Test + fun adjustsFontSizeToFitKeepsEllipsizeLocation() { + val view = TestReactTextView(RuntimeEnvironment.getApplication()) + view.setNumberOfLines(1) + view.setEllipsizeLocation(TextUtils.TruncateAt.END) + view.setAdjustFontSizeToFit(true) + + view.updateView() + + assertThat(view.ellipsize).isEqualTo(TextUtils.TruncateAt.END) + } + private fun layoutAndDraw(view: TestReactTextView, width: Int, height: Int) { view.measure( View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextLayoutManagerMinimumFontScaleTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextLayoutManagerMinimumFontScaleTest.kt new file mode 100644 index 000000000000..5fab4ab18c49 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/text/TextLayoutManagerMinimumFontScaleTest.kt @@ -0,0 +1,150 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text + +import android.text.Layout +import android.text.SpannableString +import android.text.Spanned +import android.text.TextPaint +import com.facebook.react.common.ReactConstants +import com.facebook.react.uimanager.DisplayMetricsHolder +import com.facebook.react.uimanager.PixelUtil.dpToPx +import com.facebook.react.views.text.internal.span.ReactAbsoluteSizeSpan +import com.facebook.yoga.YogaMeasureMode +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class TextLayoutManagerMinimumFontScaleTest { + + @Before + fun setUp() { + DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(RuntimeEnvironment.getApplication()) + } + + @After + fun tearDown() { + DisplayMetricsHolder.setScreenDisplayMetrics(null) + } + + @Test + fun `minimumFontScale limits how far the font shrinks relative to the largest font size`() { + val text = spannableWithFontSize(LARGE_FONT_SIZE) + + adjustToUnsatisfiableHeight(text, minimumFontScale = 0.5f) + + assertThat(largestFontSize(text)).isEqualTo((LARGE_FONT_SIZE * 0.5f).toInt()) + } + + @Test + fun `minimumFontScale is applied to the largest font size in the spannable`() { + val text = SpannableString("Small text and LARGE TEXT") + text.setSpan(ReactAbsoluteSizeSpan(SMALL_FONT_SIZE), 0, 14, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + text.setSpan( + ReactAbsoluteSizeSpan(LARGE_FONT_SIZE), + 15, + text.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + + adjustToUnsatisfiableHeight(text, minimumFontScale = 0.5f) + + assertThat(largestFontSize(text)).isEqualTo((LARGE_FONT_SIZE * 0.5f).toInt()) + } + + @Test + fun `missing minimumFontScale shrinks down to the 4dp floor`() { + val text = spannableWithFontSize(LARGE_FONT_SIZE) + + adjustToUnsatisfiableHeight(text, minimumFontScale = Float.NaN) + + assertThat(largestFontSize(text)).isEqualTo(4.dpToPx().toInt()) + } + + @Test + fun `zero minimumFontScale shrinks down to the 4dp floor`() { + val text = spannableWithFontSize(LARGE_FONT_SIZE) + + adjustToUnsatisfiableHeight(text, minimumFontScale = 0f) + + assertThat(largestFontSize(text)).isEqualTo(4.dpToPx().toInt()) + } + + @Test + fun `minimumFontScale never shrinks below the 4dp floor`() { + val text = spannableWithFontSize(LARGE_FONT_SIZE) + + adjustToUnsatisfiableHeight(text, minimumFontScale = 0.01f) + + assertThat(largestFontSize(text)).isEqualTo(4.dpToPx().toInt()) + } + + @Test + fun `text that already fits is not shrunk`() { + val text = spannableWithFontSize(LARGE_FONT_SIZE) + + TextLayoutManager.adjustSpannableFontToFit( + text, + 10_000f, + YogaMeasureMode.EXACTLY, + 10_000f, + YogaMeasureMode.EXACTLY, + 0.5f, + ReactConstants.UNSET, + true, + Layout.BREAK_STRATEGY_SIMPLE, + Layout.HYPHENATION_FREQUENCY_NONE, + Layout.Alignment.ALIGN_NORMAL, + 0, + newPaint(), + ) + + assertThat(largestFontSize(text)).isEqualTo(LARGE_FONT_SIZE) + } + + // Uses a height no font size can satisfy so the text is shrunk all the way to the minimum. + private fun adjustToUnsatisfiableHeight(text: SpannableString, minimumFontScale: Float) { + TextLayoutManager.adjustSpannableFontToFit( + text, + 10_000f, + YogaMeasureMode.EXACTLY, + 1f, + YogaMeasureMode.EXACTLY, + minimumFontScale, + ReactConstants.UNSET, + true, + Layout.BREAK_STRATEGY_SIMPLE, + Layout.HYPHENATION_FREQUENCY_NONE, + Layout.Alignment.ALIGN_NORMAL, + 0, + newPaint(), + ) + } + + private fun spannableWithFontSize(fontSize: Int): SpannableString { + val text = SpannableString("Hello") + text.setSpan(ReactAbsoluteSizeSpan(fontSize), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + return text + } + + private fun newPaint(): TextPaint = + TextPaint(TextPaint.ANTI_ALIAS_FLAG).apply { textSize = LARGE_FONT_SIZE.toFloat() } + + private fun largestFontSize(text: Spanned): Int = + text.getSpans(0, text.length, ReactAbsoluteSizeSpan::class.java).maxOfOrNull { it.size } ?: 0 + + private companion object { + const val SMALL_FONT_SIZE = 10 + const val LARGE_FONT_SIZE = 40 + } +} diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp b/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp index 98992aa2287a..32711dbeb161 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp @@ -33,8 +33,6 @@ bool ParagraphAttributes::operator==(const ParagraphAttributes& rhs) const { rhs.includeFontPadding, rhs.android_hyphenationFrequency, rhs.textAlignVertical) && - floatEquality(minimumFontSize, rhs.minimumFontSize) && - floatEquality(maximumFontSize, rhs.maximumFontSize) && floatEquality(minimumFontScale, rhs.minimumFontScale); } @@ -61,13 +59,9 @@ SharedDebugStringConvertibleList ParagraphAttributes::getDebugProps() const { adjustsFontSizeToFit, paragraphAttributes.adjustsFontSizeToFit), debugStringConvertibleItem( - "minimumFontSize", - minimumFontSize, - paragraphAttributes.minimumFontSize), - debugStringConvertibleItem( - "maximumFontSize", - maximumFontSize, - paragraphAttributes.maximumFontSize), + "minimumFontScale", + minimumFontScale, + paragraphAttributes.minimumFontScale), debugStringConvertibleItem( "includeFontPadding", includeFontPadding, diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h index 6207386a640c..4811d8063df2 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h @@ -67,18 +67,11 @@ class ParagraphAttributes : public DebugStringConvertible { */ HyphenationFrequency android_hyphenationFrequency{}; - /* - * In case of font size adjustment enabled, defines minimum and maximum - * font sizes. - */ - Float minimumFontSize{std::numeric_limits::quiet_NaN()}; - Float maximumFontSize{std::numeric_limits::quiet_NaN()}; - /* * Specifies the smallest possible scale a font can reach when * adjustsFontSizeToFit is enabled. (values 0.01-1.0). */ - Float minimumFontScale{std::numeric_limits::quiet_NaN()}; + Float minimumFontScale{0.0}; /* * The vertical alignment of the text, causing the glyphs to be vertically @@ -109,8 +102,6 @@ struct hash { attributes.textBreakStrategy, attributes.textWidthMode, attributes.adjustsFontSizeToFit, - attributes.minimumFontSize, - attributes.maximumFontSize, attributes.includeFontPadding, attributes.android_hyphenationFrequency, attributes.minimumFontScale, diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h index 4c03d715c35d..ed876b51d45c 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h @@ -1085,18 +1085,6 @@ inline ParagraphAttributes convertRawProp( "minimumFontScale", sourceParagraphAttributes.minimumFontScale, defaultParagraphAttributes.minimumFontScale); - paragraphAttributes.minimumFontSize = convertRawProp( - context, - rawProps, - "minimumFontSize", - sourceParagraphAttributes.minimumFontSize, - defaultParagraphAttributes.minimumFontSize); - paragraphAttributes.maximumFontSize = convertRawProp( - context, - rawProps, - "maximumFontSize", - sourceParagraphAttributes.maximumFontSize, - defaultParagraphAttributes.maximumFontSize); paragraphAttributes.includeFontPadding = convertRawProp( context, rawProps, @@ -1199,8 +1187,7 @@ constexpr static MapBuffer::Key PA_KEY_TEXT_BREAK_STRATEGY = 2; constexpr static MapBuffer::Key PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3; constexpr static MapBuffer::Key PA_KEY_INCLUDE_FONT_PADDING = 4; constexpr static MapBuffer::Key PA_KEY_HYPHENATION_FREQUENCY = 5; -constexpr static MapBuffer::Key PA_KEY_MINIMUM_FONT_SIZE = 6; -constexpr static MapBuffer::Key PA_KEY_MAXIMUM_FONT_SIZE = 7; +constexpr static MapBuffer::Key PA_KEY_MINIMUM_FONT_SCALE = 6; constexpr static MapBuffer::Key PA_KEY_TEXT_ALIGN_VERTICAL = 8; constexpr static MapBuffer::Key PA_KEY_TEXT_WIDTH_MODE = 9; @@ -1217,8 +1204,7 @@ inline MapBuffer toMapBuffer(const ParagraphAttributes ¶graphAttributes) if (paragraphAttributes.textAlignVertical.has_value()) { builder.putString(PA_KEY_TEXT_ALIGN_VERTICAL, toString(*paragraphAttributes.textAlignVertical)); } - builder.putDouble(PA_KEY_MINIMUM_FONT_SIZE, paragraphAttributes.minimumFontSize); - builder.putDouble(PA_KEY_MAXIMUM_FONT_SIZE, paragraphAttributes.maximumFontSize); + builder.putDouble(PA_KEY_MINIMUM_FONT_SCALE, paragraphAttributes.minimumFontScale); return builder.build(); } diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/tests/ParagraphAttributesTest.cpp b/packages/react-native/ReactCommon/react/renderer/attributedstring/tests/ParagraphAttributesTest.cpp index 3b395a1d9065..0a084379091e 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/tests/ParagraphAttributesTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/tests/ParagraphAttributesTest.cpp @@ -9,11 +9,11 @@ #include #include +#include + namespace facebook::react { -// The three Float fields default to NaN, and NaN != NaN under IEEE-754. -// operator== must special-case NaN via floatEquality so two freshly -// default-constructed ParagraphAttributes compare equal. +// Two freshly default-constructed ParagraphAttributes must compare equal. TEST( ParagraphAttributesTest, testOperatorEqualsDefaultConstructedInstancesAreEqual) { @@ -23,38 +23,36 @@ TEST( EXPECT_TRUE(a == b); } -// operator== compares Float fields with an epsilon tolerance (0.005) rather -// than an exact ==. Differences below the epsilon must still compare equal; -// differences well above the epsilon must compare unequal. +// operator== compares minimumFontScale with an epsilon tolerance (0.005) +// rather than an exact ==. Differences below the epsilon must still compare +// equal; differences well above the epsilon must compare unequal. TEST( ParagraphAttributesTest, testOperatorEqualsFloatFieldsUseEpsilonComparison) { ParagraphAttributes a{}; - a.minimumFontSize = 12.0f; - a.maximumFontSize = 48.0f; a.minimumFontScale = 0.5f; auto b = a; - b.minimumFontSize = a.minimumFontSize + 0.001f; - b.maximumFontSize = a.maximumFontSize + 0.001f; b.minimumFontScale = a.minimumFontScale + 0.001f; EXPECT_TRUE(a == b); b = a; - b.minimumFontSize = a.minimumFontSize + 1.0f; + b.minimumFontScale = a.minimumFontScale + 0.1f; EXPECT_FALSE(a == b); } // floatEquality returns true only when *both* operands are NaN or when -// *neither* is. A NaN-vs-finite mismatch in any of the three float fields -// must therefore make the instances unequal, even though both operands are -// "invalid" font sizes. -TEST( - ParagraphAttributesTest, - testOperatorEqualsNaNVsFiniteFloatComparesUnequal) { +// *neither* is. Two NaN minimumFontScale values must compare equal, and a +// NaN-vs-finite mismatch must compare unequal. +TEST(ParagraphAttributesTest, testOperatorEqualsHandlesNaNMinimumFontScale) { ParagraphAttributes withNaN{}; + withNaN.minimumFontScale = std::numeric_limits::quiet_NaN(); + auto otherWithNaN = withNaN; + + EXPECT_TRUE(withNaN == otherWithNaN); + ParagraphAttributes withFinite{}; - withFinite.minimumFontSize = 12.0f; + withFinite.minimumFontScale = 0.5f; EXPECT_FALSE(withNaN == withFinite); } diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/BaseParagraphProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/BaseParagraphProps.cpp index e0fcc551392e..deb1c44c5bfa 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/BaseParagraphProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/BaseParagraphProps.cpp @@ -98,18 +98,6 @@ void BaseParagraphProps::setProp( paragraphAttributes, minimumFontScale, "minimumFontScale"); - REBUILD_FIELD_SWITCH_CASE( - paDefaults, - value, - paragraphAttributes, - minimumFontSize, - "minimumFontSize"); - REBUILD_FIELD_SWITCH_CASE( - paDefaults, - value, - paragraphAttributes, - maximumFontSize, - "maximumFontSize"); REBUILD_FIELD_SWITCH_CASE( paDefaults, value, diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/platform/android/react/renderer/components/text/HostPlatformParagraphProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/platform/android/react/renderer/components/text/HostPlatformParagraphProps.cpp index a9a6f1452c1c..a56f60823459 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/platform/android/react/renderer/components/text/HostPlatformParagraphProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/platform/android/react/renderer/components/text/HostPlatformParagraphProps.cpp @@ -122,18 +122,6 @@ folly::dynamic HostPlatformParagraphProps::getDiffProps( result["minimumFontScale"] = paragraphAttributes.minimumFontScale; } - if (!floatEquality( - paragraphAttributes.minimumFontSize, - oldProps->paragraphAttributes.minimumFontSize)) { - result["minimumFontSize"] = paragraphAttributes.minimumFontSize; - } - - if (!floatEquality( - paragraphAttributes.maximumFontSize, - oldProps->paragraphAttributes.maximumFontSize)) { - result["maximumFontSize"] = paragraphAttributes.maximumFontSize; - } - if (paragraphAttributes.includeFontPadding != oldProps->paragraphAttributes.includeFontPadding) { result["includeFontPadding"] = paragraphAttributes.includeFontPadding; diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp index ac3adc4d4ed2..3c8f35198473 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp @@ -177,14 +177,8 @@ void BaseTextInputProps::setProp( paDefaults, value, paragraphAttributes, - minimumFontSize, - "minimumFontSize"); - REBUILD_FIELD_SWITCH_CASE( - paDefaults, - value, - paragraphAttributes, - maximumFontSize, - "maximumFontSize"); + minimumFontScale, + "minimumFontScale"); REBUILD_FIELD_SWITCH_CASE( paDefaults, value, diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp index 60b14966a648..0aa01288075a 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp @@ -390,15 +390,9 @@ folly::dynamic AndroidTextInputProps::getDiffProps( } if (!floatEquality( - paragraphAttributes.minimumFontSize, - oldProps->paragraphAttributes.minimumFontSize)) { - result["minimumFontSize"] = paragraphAttributes.minimumFontSize; - } - - if (!floatEquality( - paragraphAttributes.maximumFontSize, - oldProps->paragraphAttributes.maximumFontSize)) { - result["maximumFontSize"] = paragraphAttributes.maximumFontSize; + paragraphAttributes.minimumFontScale, + oldProps->paragraphAttributes.minimumFontScale)) { + result["minimumFontScale"] = paragraphAttributes.minimumFontScale; } if (paragraphAttributes.includeFontPadding != 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 171f5f67b618..44606ea4a6e0 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 @@ -415,6 +415,22 @@ - (LinesMeasurements)getLinesForAttributedString:(facebook::react::AttributedStr return paragraphLines; } +- (CGFloat)_maximumFontSizeInAttributedString:(NSAttributedString *)attributedString +{ + __block CGFloat maximumFontSize = 0.0; + [attributedString enumerateAttribute:NSFontAttributeName + inRange:NSMakeRange(0, attributedString.length) + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(id _Nullable value, NSRange range, BOOL *_Nonnull stop) { + CGFloat fontSize = ((UIFont *)value).pointSize; + if (fontSize > maximumFontSize) { + maximumFontSize = fontSize; + } + }]; + + return maximumFontSize; +} + - (NSTextStorage *)_textStorageAndLayoutManagerWithAttributesString:(NSAttributedString *)attributedString paragraphAttributes:(ParagraphAttributes)paragraphAttributes size:(CGSize)size @@ -438,8 +454,8 @@ - (NSTextStorage *)_textStorageAndLayoutManagerWithAttributesString:(NSAttribute [textStorage addLayoutManager:layoutManager]; if (paragraphAttributes.adjustsFontSizeToFit) { - CGFloat minimumFontSize = !isnan(paragraphAttributes.minimumFontSize) ? paragraphAttributes.minimumFontSize : 4.0; - CGFloat maximumFontSize = !isnan(paragraphAttributes.maximumFontSize) ? paragraphAttributes.maximumFontSize : 96.0; + CGFloat maximumFontSize = [self _maximumFontSizeInAttributedString:attributedString]; + CGFloat minimumFontSize = MAX(paragraphAttributes.minimumFontScale * maximumFontSize, 4.0); [textStorage scaleFontSizeToFitSize:size minimumFontSize:minimumFontSize maximumFontSize:maximumFontSize]; } diff --git a/packages/rn-tester/js/examples/Text/TextExample.android.js b/packages/rn-tester/js/examples/Text/TextExample.android.js index 7be080f10715..39b1c94aa248 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.android.js +++ b/packages/rn-tester/js/examples/Text/TextExample.android.js @@ -154,6 +154,14 @@ class AdjustingFontSize extends React.Component< Shrinking to fit available space is much better! + + Can limit how small the text becomes with minimumFontScale + + + + Can limit how small the text becomes with minimumFontScale + +