From dd525b2484c474b87d61d5ba3e52631116574b2b Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Mon, 18 Mar 2024 23:19:52 -0400 Subject: [PATCH 1/3] Fix adjustsFontSizeToFit on iOS Fabric --- .../attributedstring/ParagraphAttributes.cpp | 6 ++---- .../attributedstring/ParagraphAttributes.h | 10 ++++------ .../renderer/attributedstring/conversions.h | 14 ++++--------- .../components/text/ParagraphProps.cpp | 10 ++-------- .../textinput/BaseTextInputProps.cpp | 10 ++-------- .../textlayoutmanager/RCTTextLayoutManager.mm | 20 +++++++++++++++++-- 6 files changed, 32 insertions(+), 38 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp b/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp index f2317ba080f2..23650236a1f2 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp @@ -29,8 +29,7 @@ bool ParagraphAttributes::operator==(const ParagraphAttributes& rhs) const { rhs.adjustsFontSizeToFit, rhs.includeFontPadding, rhs.android_hyphenationFrequency) && - floatEquality(minimumFontSize, rhs.minimumFontSize) && - floatEquality(maximumFontSize, rhs.maximumFontSize); + floatEquality(minimumFontScale, rhs.minimumFontScale); } bool ParagraphAttributes::operator!=(const ParagraphAttributes& rhs) const { @@ -46,8 +45,7 @@ SharedDebugStringConvertibleList ParagraphAttributes::getDebugProps() const { debugStringConvertibleItem("ellipsizeMode", ellipsizeMode), debugStringConvertibleItem("textBreakStrategy", textBreakStrategy), debugStringConvertibleItem("adjustsFontSizeToFit", adjustsFontSizeToFit), - debugStringConvertibleItem("minimumFontSize", minimumFontSize), - debugStringConvertibleItem("maximumFontSize", maximumFontSize), + debugStringConvertibleItem("minimumFontScale", minimumFontScale), debugStringConvertibleItem("includeFontPadding", includeFontPadding), debugStringConvertibleItem( "android_hyphenationFrequency", android_hyphenationFrequency)}; diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h index d73f8632b84f..7db6e6962b3b 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h @@ -64,11 +64,10 @@ class ParagraphAttributes : public DebugStringConvertible { HyphenationFrequency android_hyphenationFrequency{}; /* - * In case of font size adjustment enabled, defines minimum and maximum - * font sizes. + * Specifies smallest possible scale a font can reach when adjustsFontSizeToFit + * is enabled. (values 0.01-1.0). */ - Float minimumFontSize{std::numeric_limits::quiet_NaN()}; - Float maximumFontSize{std::numeric_limits::quiet_NaN()}; + Float minimumFontScale{0.0}; bool operator==(const ParagraphAttributes&) const; bool operator!=(const ParagraphAttributes&) const; @@ -93,8 +92,7 @@ struct hash { attributes.ellipsizeMode, attributes.textBreakStrategy, attributes.adjustsFontSizeToFit, - attributes.minimumFontSize, - attributes.maximumFontSize, + attributes.minimumFontScale, attributes.includeFontPadding, attributes.android_hyphenationFrequency); } diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h index a975fe080d9b..61f21f52dbd1 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h @@ -762,18 +762,12 @@ inline ParagraphAttributes convertRawProp( "adjustsFontSizeToFit", sourceParagraphAttributes.adjustsFontSizeToFit, defaultParagraphAttributes.adjustsFontSizeToFit); - paragraphAttributes.minimumFontSize = convertRawProp( + paragraphAttributes.minimumFontScale = convertRawProp( context, rawProps, - "minimumFontSize", - sourceParagraphAttributes.minimumFontSize, - defaultParagraphAttributes.minimumFontSize); - paragraphAttributes.maximumFontSize = convertRawProp( - context, - rawProps, - "maximumFontSize", - sourceParagraphAttributes.maximumFontSize, - defaultParagraphAttributes.maximumFontSize); + "minimumFontScale", + sourceParagraphAttributes.minimumFontScale, + defaultParagraphAttributes.minimumFontScale); paragraphAttributes.includeFontPadding = convertRawProp( context, rawProps, diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.cpp index 4011bbf2dc8f..309af6f3a2e2 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.cpp @@ -98,14 +98,8 @@ void ParagraphProps::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/BaseTextInputProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp index ec0f350b5a47..e004b70ae0ab 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp @@ -145,14 +145,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/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm index b17124642b53..4e65c802ce2c 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 @@ -167,6 +167,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 @@ -188,8 +204,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]; } From 11e84114d91040c4354826594cbc0ba4c695c89b Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Mon, 18 Mar 2024 23:29:38 -0400 Subject: [PATCH 2/3] Add RNTester examples --- .../js/examples/Text/TextExample.android.js | 16 ++++++++++++++++ .../js/examples/Text/TextExample.ios.js | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/packages/rn-tester/js/examples/Text/TextExample.android.js b/packages/rn-tester/js/examples/Text/TextExample.android.js index c8acafe10e96..2615d0ef9394 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.android.js +++ b/packages/rn-tester/js/examples/Text/TextExample.android.js @@ -134,6 +134,22 @@ class AdjustingFontSize extends React.Component< style={{fontSize: 36, marginVertical: 6}}> Truncated text is baaaaad. + + + It doesn't grow + + + + Can limit how small the text becomes with minimumFontScale + + Truncated text is baaaaad. + + + It doesn't grow + + + + Can limit how small the text becomes with minimumFontScale + + Date: Wed, 19 Jun 2024 13:18:24 -0400 Subject: [PATCH 3/3] Fix android --- .../ReactAndroid/api/ReactAndroid.api | 4 +-- .../react/views/text/ReactTextView.java | 10 +++--- .../views/text/ReactTextViewManager.java | 8 ++--- .../react/views/text/TextLayoutManager.java | 36 +++++++++---------- .../renderer/attributedstring/conversions.h | 7 ++-- 5 files changed, 29 insertions(+), 36 deletions(-) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index cad0f20e2ba3..a17e7cbd46e7 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -7390,7 +7390,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 setNotifyOnInlineViewLayout (Z)V public fun setNumberOfLines (I)V public fun setOverflow (Ljava/lang/String;)V @@ -7606,7 +7606,7 @@ public class com/facebook/react/views/text/TextLayoutManager { public static final field PA_KEY_INCLUDE_FONT_PADDING S public static final field PA_KEY_MAXIMUM_FONT_SIZE S public static final field PA_KEY_MAX_NUMBER_OF_LINES S - public static final field PA_KEY_MINIMUM_FONT_SIZE S + public static final field PA_KEY_MINIMUM_FONT_SCALE S public static final field PA_KEY_TEXT_BREAK_STRATEGY S public fun ()V public static fun adjustSpannableFontToFit (Landroid/text/Spannable;FLcom/facebook/yoga/YogaMeasureMode;FLcom/facebook/yoga/YogaMeasureMode;DIZIILandroid/text/Layout$Alignment;)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 424160192fc1..8727f4d1f96c 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 @@ -63,7 +63,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie private TextUtils.TruncateAt mEllipsizeLocation; private boolean mAdjustsFontSizeToFit; private float mFontSize; - private float mMinimumFontSize; + private float mMinimumFontScale; private float mLetterSpacing; private int mLinkifyMaskType; private boolean mNotifyOnInlineViewLayout; @@ -99,7 +99,7 @@ private void initView() { mShouldAdjustSpannableFontSize = false; mEllipsizeLocation = TextUtils.TruncateAt.END; mFontSize = Float.NaN; - mMinimumFontSize = Float.NaN; + mMinimumFontScale = 0.f; mLetterSpacing = 0.f; mSpanned = null; @@ -371,7 +371,7 @@ protected void onDraw(Canvas canvas) { YogaMeasureMode.EXACTLY, getHeight(), YogaMeasureMode.EXACTLY, - mMinimumFontSize, + mMinimumFontScale, mNumberOfLines, getIncludeFontPadding(), getBreakStrategy(), @@ -623,8 +623,8 @@ public void setFontSize(float fontSize) { applyTextAttributes(); } - public void setMinimumFontSize(float minimumFontSize) { - mMinimumFontSize = minimumFontSize; + public void setMinimumFontScale(float minimumFontScale) { + mMinimumFontScale = minimumFontScale; mShouldAdjustSpannableFontSize = true; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java index 8ca3155b31a6..edcf05043bb1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java @@ -154,11 +154,11 @@ private Object getReactTextUpdate(ReactTextView view, ReactStylesDiffMap props, view.setSpanned(spanned); try { - float minimumFontSize = - (float) paragraphAttributes.getDouble(TextLayoutManager.PA_KEY_MINIMUM_FONT_SIZE); - view.setMinimumFontSize(minimumFontSize); + float minimumFontScale = + (float) paragraphAttributes.getDouble(TextLayoutManager.PA_KEY_MINIMUM_FONT_SCALE); + view.setMinimumFontScale(minimumFontScale); } catch (IllegalArgumentException e) { - // T190482857: We see rare crash with MapBuffer without PA_KEY_MINIMUM_FONT_SIZE entry + // T190482857: We see rare crash with MapBuffer without PA_KEY_MINIMUM_FONT_SCALE entry FLog.e( TAG, "Paragraph Attributes: %s", diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java index 2921f84140d1..2ee409e02b1f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java @@ -77,8 +77,7 @@ public class TextLayoutManager { public static final short PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3; public static final short PA_KEY_INCLUDE_FONT_PADDING = 4; public static final short PA_KEY_HYPHENATION_FREQUENCY = 5; - public static final short PA_KEY_MINIMUM_FONT_SIZE = 6; - public static final short PA_KEY_MAXIMUM_FONT_SIZE = 7; + public static final short PA_KEY_MINIMUM_FONT_SCALE = 6; private static final boolean ENABLE_MEASURE_LOGGING = ReactBuildConfig.DEBUG && false; @@ -428,7 +427,7 @@ public static void adjustSpannableFontToFit( YogaMeasureMode widthYogaMeasureMode, float height, YogaMeasureMode heightYogaMeasureMode, - double minimumFontSizeAttr, + double minimumFontScaleAttr, int maximumNumberOfLines, boolean includeFontPadding, int textBreakStrategy, @@ -446,18 +445,15 @@ public static void adjustSpannableFontToFit( hyphenationFrequency, alignment); - // Minimum font size is 4pts to match the iOS implementation. - int minimumFontSize = - (int) - (Double.isNaN(minimumFontSizeAttr) ? PixelUtil.toPixelFromDIP(4) : minimumFontSizeAttr); - - // Find the largest font size used in the spannable to use as a starting point. - int currentFontSize = minimumFontSize; + // Find the largest font size used in the spanable to use as a starting point. + int currentFontSize = 0; ReactAbsoluteSizeSpan[] spans = text.getSpans(0, text.length(), ReactAbsoluteSizeSpan.class); for (ReactAbsoluteSizeSpan span : spans) { currentFontSize = Math.max(currentFontSize, span.getSize()); } + // Minimum font size is 4pts to match the iOS implementation. + int minimumFontSize = (int) Math.max(minimumFontScaleAttr * currentFontSize, PixelUtil.toPixelFromDIP(4)); int initialFontSize = currentFontSize; while (currentFontSize > minimumFontSize && ((maximumNumberOfLines != ReactConstants.UNSET @@ -534,10 +530,10 @@ public static long measureText( Layout.Alignment alignment = getTextAlignment(attributedString, text); if (adjustFontSizeToFit) { - double minimumFontSize = - paragraphAttributes.contains(PA_KEY_MINIMUM_FONT_SIZE) - ? paragraphAttributes.getDouble(PA_KEY_MINIMUM_FONT_SIZE) - : Double.NaN; + double minimumFontScale = + paragraphAttributes.contains(PA_KEY_MINIMUM_FONT_SCALE) + ? paragraphAttributes.getDouble(PA_KEY_MINIMUM_FONT_SCALE) + : 0.0; adjustSpannableFontToFit( text, @@ -545,7 +541,7 @@ public static long measureText( widthYogaMeasureMode, height, heightYogaMeasureMode, - minimumFontSize, + minimumFontScale, maximumNumberOfLines, includeFontPadding, textBreakStrategy, @@ -743,10 +739,10 @@ public static WritableArray measureLines( Layout.Alignment alignment = getTextAlignment(attributedString, text); if (adjustFontSizeToFit) { - double minimumFontSize = - paragraphAttributes.contains(PA_KEY_MINIMUM_FONT_SIZE) - ? paragraphAttributes.getDouble(PA_KEY_MINIMUM_FONT_SIZE) - : Double.NaN; + double minimumFontScale = + paragraphAttributes.contains(PA_KEY_MINIMUM_FONT_SCALE) + ? paragraphAttributes.getDouble(PA_KEY_MINIMUM_FONT_SCALE) + : 0.0; adjustSpannableFontToFit( text, @@ -754,7 +750,7 @@ public static WritableArray measureLines( YogaMeasureMode.EXACTLY, height, YogaMeasureMode.UNDEFINED, - minimumFontSize, + minimumFontScale, maximumNumberOfLines, includeFontPadding, textBreakStrategy, diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h index 61f21f52dbd1..52e2e9609f31 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h @@ -858,8 +858,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; inline MapBuffer toMapBuffer(const ParagraphAttributes& paragraphAttributes) { auto builder = MapBufferBuilder(); @@ -878,9 +877,7 @@ inline MapBuffer toMapBuffer(const ParagraphAttributes& paragraphAttributes) { PA_KEY_HYPHENATION_FREQUENCY, toString(paragraphAttributes.android_hyphenationFrequency)); builder.putDouble( - PA_KEY_MINIMUM_FONT_SIZE, paragraphAttributes.minimumFontSize); - builder.putDouble( - PA_KEY_MAXIMUM_FONT_SIZE, paragraphAttributes.maximumFontSize); + PA_KEY_MINIMUM_FONT_SCALE, paragraphAttributes.minimumFontScale); return builder.build(); }