Skip to content

Commit 2b08f9a

Browse files
author
Hayate Aizawa
committed
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.
1 parent 618b844 commit 2b08f9a

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

‎packages/react-native/Libraries/Text/Text/NSTextStorage+FontScaling.m‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
#import "NSTextStorage+FontScaling.h"
99

10+
#import <React/RCTUtils.h>
11+
1012
typedef NS_OPTIONS(NSInteger, RCTTextSizeComparisonOptions) {
1113
RCTTextSizeComparisonSmaller = 1 << 0,
1214
RCTTextSizeComparisonLarger = 1 << 1,
@@ -87,8 +89,10 @@ - (RCTTextSizeComparisonOptions)compareToSize:(CGSize)size thresholdRatio:(CGFlo
8789

8890
CGSize measuredSize = [layoutManager usedRectForTextContainer:textContainer].size;
8991

90-
// Does it fit the size?
91-
BOOL fitsSize = size.width >= measuredSize.width && size.height >= measuredSize.height;
92+
// Does it fit the size? Yoga's pixel-grid rounding can leave the height a float ULP short of
93+
// the line height (#52642), so a shortfall smaller than one physical pixel still counts as fitting.
94+
CGFloat heightTolerance = 1.0 / RCTScreenScale();
95+
BOOL fitsSize = size.width >= measuredSize.width && size.height + heightTolerance > measuredSize.height;
9296

9397
CGSize thresholdSize = (CGSize){
9498
size.width * thresholdRatio,

‎packages/rn-tester/js/examples/Text/TextExample.ios.js‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,39 @@ class AdjustingFontSize extends React.Component<
278278
}
279279
}
280280

281+
function FontSizeToFitWithLineHeightRow({index}: {index: number}) {
282+
const [height, setHeight] = React.useState<?number>(null);
283+
return (
284+
<View style={{flexDirection: 'row', alignItems: 'center'}}>
285+
<Text
286+
numberOfLines={1}
287+
adjustsFontSizeToFit={true}
288+
minimumFontScale={0.8}
289+
onLayout={e => setHeight(e.nativeEvent.layout.height)}
290+
style={{fontSize: 12, lineHeight: 20, width: 200}}>
291+
{'Should not shrink ' + (index + 1)}
292+
</Text>
293+
<Text style={{fontSize: 9, color: '#888'}}>
294+
{height == null ? '' : String(height)}
295+
</Text>
296+
</View>
297+
);
298+
}
299+
300+
// Rows after a 0.5pt separator: on a 3x screen some get a frame a float ULP short of lineHeight (#52642).
301+
function FontSizeToFitWithLineHeightExample(): React.Node {
302+
const rows = [];
303+
for (let i = 0; i < 10; i++) {
304+
rows.push(<FontSizeToFitWithLineHeightRow key={i} index={i} />);
305+
}
306+
return (
307+
<View style={{alignItems: 'center'}}>
308+
<View style={{height: 0.5, width: 200, backgroundColor: 'green'}} />
309+
{rows}
310+
</View>
311+
);
312+
}
313+
281314
class TextBaseLineLayoutExample extends React.Component<{}, unknown> {
282315
render(): React.Node {
283316
const texts = [];
@@ -1424,6 +1457,13 @@ const examples = [
14241457
return <AdjustingFontSize />;
14251458
},
14261459
},
1460+
{
1461+
title: 'Font Size Adjustment with lineHeight',
1462+
name: 'adjustsFontSizeToFitLineHeight',
1463+
render: function (): React.Node {
1464+
return <FontSizeToFitWithLineHeightExample />;
1465+
},
1466+
},
14271467
{
14281468
title: 'Text Align with RTL',
14291469
name: 'textAlign',

0 commit comments

Comments
 (0)