Skip to content

Commit 9075389

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 9075389

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

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

Lines changed: 11 additions & 1 deletion
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,
@@ -88,7 +90,15 @@ - (RCTTextSizeComparisonOptions)compareToSize:(CGSize)size thresholdRatio:(CGFlo
8890
CGSize measuredSize = [layoutManager usedRectForTextContainer:textContainer].size;
8991

9092
// Does it fit the size?
91-
BOOL fitsSize = size.width >= measuredSize.width && size.height >= measuredSize.height;
93+
// Yoga snaps each edge of a node to the pixel grid independently and derives the height from
94+
// their difference, so on screens whose scale is not a power of two the frame can come out one
95+
// float ULP short of the requested `lineHeight` once the node sits far enough down the screen
96+
// (e.g. 19.999992 for `lineHeight: 20`, see react/yoga#2011). Because the line height does not
97+
// change when the font is scaled, such a frame never "fits" and the text collapses to
98+
// `minimumFontSize` (react/react-native#52642). A shortfall smaller than one physical pixel
99+
// cannot change what is drawn, so it is treated as fitting.
100+
CGFloat heightTolerance = 1.0 / RCTScreenScale();
101+
BOOL fitsSize = size.width >= measuredSize.width && size.height + heightTolerance > measuredSize.height;
92102

93103
CGSize thresholdSize = (CGSize){
94104
size.width * thresholdRatio,

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,44 @@ 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+
/**
301+
* Rows with a fixed `lineHeight` placed after a fractional-height separator. Yoga rounds
302+
* each edge to the pixel grid independently, so on a 3x screen some rows get a frame one
303+
* float ULP shorter than the line height; without a tolerance in the fitting check those
304+
* rows were scaled down to the minimum font size even though the text fits (#52642).
305+
*/
306+
function FontSizeToFitWithLineHeightExample(): React.Node {
307+
const rows = [];
308+
for (let i = 0; i < 10; i++) {
309+
rows.push(<FontSizeToFitWithLineHeightRow key={i} index={i} />);
310+
}
311+
return (
312+
<View style={{alignItems: 'center'}}>
313+
<View style={{height: 0.5, width: 200, backgroundColor: 'green'}} />
314+
{rows}
315+
</View>
316+
);
317+
}
318+
281319
class TextBaseLineLayoutExample extends React.Component<{}, unknown> {
282320
render(): React.Node {
283321
const texts = [];
@@ -1424,6 +1462,13 @@ const examples = [
14241462
return <AdjustingFontSize />;
14251463
},
14261464
},
1465+
{
1466+
title: 'Font Size Adjustment with lineHeight',
1467+
name: 'adjustsFontSizeToFitLineHeight',
1468+
render: function (): React.Node {
1469+
return <FontSizeToFitWithLineHeightExample />;
1470+
},
1471+
},
14271472
{
14281473
title: 'Text Align with RTL',
14291474
name: 'textAlign',

0 commit comments

Comments
 (0)