From 4c3d6da57fe8e686550150c4a6a7e7f31dc7b6dc Mon Sep 17 00:00:00 2001 From: Hayate Aizawa <152232532+nowex35@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:32:49 +0900 Subject: [PATCH 1/2] Tolerate sub-pixel height shortfall in adjustsFontSizeToFit on iOS Yoga snaps each edge of a node to the pixel grid independently and derives the height from their difference. On screens whose scale is not a power of two, a node far enough down the screen can get a frame one float ULP short of its `lineHeight` (19.999992 for 20, react/yoga#2011). `compareToSize:` had no tolerance on the height, and the line height does not change when the font is scaled, so such a row never fit and was scaled down to `minimumFontSize` even though the text fit (#52642). Treat a height shortfall smaller than one physical pixel as fitting. Width keeps its existing check. Adds an RNTester example that reproduces the condition on a 3x device. --- .../Text/Text/NSTextStorage+FontScaling.m | 8 +++- .../js/examples/Text/TextExample.ios.js | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.m b/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.m index c92b2e94c6a1..83e5f82076e2 100644 --- a/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.m +++ b/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.m @@ -7,6 +7,8 @@ #import "NSTextStorage+FontScaling.h" +#import + typedef NS_OPTIONS(NSInteger, RCTTextSizeComparisonOptions) { RCTTextSizeComparisonSmaller = 1 << 0, RCTTextSizeComparisonLarger = 1 << 1, @@ -87,8 +89,10 @@ - (RCTTextSizeComparisonOptions)compareToSize:(CGSize)size thresholdRatio:(CGFlo CGSize measuredSize = [layoutManager usedRectForTextContainer:textContainer].size; - // Does it fit the size? - BOOL fitsSize = size.width >= measuredSize.width && size.height >= measuredSize.height; + // Does it fit the size? Yoga's pixel-grid rounding can leave the height a float ULP short of + // the line height (#52642), so a shortfall smaller than one physical pixel still counts as fitting. + CGFloat heightTolerance = 1.0 / RCTScreenScale(); + BOOL fitsSize = size.width >= measuredSize.width && size.height + heightTolerance > measuredSize.height; CGSize thresholdSize = (CGSize){ size.width * thresholdRatio, diff --git a/packages/rn-tester/js/examples/Text/TextExample.ios.js b/packages/rn-tester/js/examples/Text/TextExample.ios.js index e367974e3da2..b0ded7dd8cfb 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.ios.js +++ b/packages/rn-tester/js/examples/Text/TextExample.ios.js @@ -278,6 +278,39 @@ class AdjustingFontSize extends React.Component< } } +function FontSizeToFitWithLineHeightRow({index}: {index: number}) { + const [height, setHeight] = React.useState(null); + return ( + + setHeight(e.nativeEvent.layout.height)} + style={{fontSize: 12, lineHeight: 20, width: 200}}> + {'Should not shrink ' + (index + 1)} + + + {height == null ? '' : String(height)} + + + ); +} + +// Rows after a 0.5pt separator: on a 3x screen some get a frame a float ULP short of lineHeight (#52642). +function FontSizeToFitWithLineHeightExample(): React.Node { + const rows = []; + for (let i = 0; i < 10; i++) { + rows.push(); + } + return ( + + + {rows} + + ); +} + class TextBaseLineLayoutExample extends React.Component<{}, unknown> { render(): React.Node { const texts = []; @@ -1424,6 +1457,13 @@ const examples = [ return ; }, }, + { + title: 'Font Size Adjustment with lineHeight', + name: 'adjustsFontSizeToFitLineHeight', + render: function (): React.Node { + return ; + }, + }, { title: 'Text Align with RTL', name: 'textAlign', From 6b33da33289b29eb1a2766b4f2e9bd20ce390776 Mon Sep 17 00:00:00 2001 From: Hayate Aizawa <152232532+nowex35@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:32:49 +0900 Subject: [PATCH 2/2] Compare heights on the pixel grid instead of a fixed tolerance Round both the container height and the measured text height with RCTRoundPixelValue before comparing them, instead of accepting any shortfall smaller than one physical pixel. The float error from Yoga's pixel-grid snapping (#52642) still rounds away, while a container that is genuinely a pixel short of the text no longer counts as fitting. --- .../Libraries/Text/Text/NSTextStorage+FontScaling.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.m b/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.m index 83e5f82076e2..2a6feee71b75 100644 --- a/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.m +++ b/packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.m @@ -90,9 +90,9 @@ - (RCTTextSizeComparisonOptions)compareToSize:(CGSize)size thresholdRatio:(CGFlo CGSize measuredSize = [layoutManager usedRectForTextContainer:textContainer].size; // Does it fit the size? Yoga's pixel-grid rounding can leave the height a float ULP short of - // the line height (#52642), so a shortfall smaller than one physical pixel still counts as fitting. - CGFloat heightTolerance = 1.0 / RCTScreenScale(); - BOOL fitsSize = size.width >= measuredSize.width && size.height + heightTolerance > measuredSize.height; + // the line height (#52642), so both heights are rounded to the pixel grid before comparing. + BOOL fitsSize = size.width >= measuredSize.width && + RCTRoundPixelValue(size.height) >= RCTRoundPixelValue(measuredSize.height); CGSize thresholdSize = (CGSize){ size.width * thresholdRatio,