Skip to content

Commit d471955

Browse files
expand text view frame and drawAttributedString frame when lineheight < fontsize.lineheight
1 parent d1809f0 commit d471955

3 files changed

Lines changed: 106 additions & 1 deletion

File tree

packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ @interface RCTParagraphTextView : UIView
3232
@property (nonatomic) ParagraphAttributes paragraphAttributes;
3333
@property (nonatomic) LayoutMetrics layoutMetrics;
3434

35+
// How much glyphs extend beyond line height on each side (when lineHeight < font line height).
36+
- (CGFloat)glyphOverflow;
37+
// Recomputes glyphOverflow from the current attributed string. Call after state changes.
38+
- (void)updateGlyphOverflow;
39+
3540
@end
3641

3742
#if !TARGET_OS_TV
@@ -123,6 +128,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
123128
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState
124129
{
125130
_textView.state = std::static_pointer_cast<const ParagraphShadowNode::ConcreteState>(state);
131+
[_textView updateGlyphOverflow];
126132
[_textView setNeedsDisplay];
127133
[self setNeedsLayout];
128134
}
@@ -142,14 +148,25 @@ - (void)prepareForRecycle
142148
{
143149
[super prepareForRecycle];
144150
_textView.state = nullptr;
151+
[_textView updateGlyphOverflow];
145152
_accessibilityProvider = nil;
146153
}
147154

148155
- (void)layoutSubviews
149156
{
150157
[super layoutSubviews];
151158

152-
_textView.frame = self.bounds;
159+
CGFloat overflow = _textView.glyphOverflow;
160+
if (overflow > 0) {
161+
// When lineHeight < font line height, expand the frame so glyphs can be rendered to match web behaiour.
162+
_textView.frame = CGRectMake(
163+
self.bounds.origin.x,
164+
self.bounds.origin.y - overflow,
165+
self.bounds.size.width,
166+
self.bounds.size.height + overflow * 2);
167+
} else {
168+
_textView.frame = self.bounds;
169+
}
153170
}
154171

155172
#pragma mark - Accessibility
@@ -382,6 +399,58 @@ - (void)disableContextMenu
382399

383400
@implementation RCTParagraphTextView {
384401
CAShapeLayer *_highlightLayer;
402+
CGFloat _glyphOverflow;
403+
}
404+
405+
- (CGFloat)glyphOverflow
406+
{
407+
return _glyphOverflow;
408+
}
409+
410+
- (void)updateGlyphOverflow
411+
{
412+
_glyphOverflow = 0;
413+
414+
if (!_state) {
415+
return;
416+
}
417+
418+
NSAttributedString *attributedText =
419+
RCTNSAttributedStringFromAttributedString(_state->getData().attributedString);
420+
if (!attributedText || attributedText.length == 0) {
421+
return;
422+
}
423+
424+
__block CGFloat maxLineHeight = 0;
425+
__block CGFloat maxFontLineHeight = 0;
426+
427+
[attributedText enumerateAttribute:NSParagraphStyleAttributeName
428+
inRange:NSMakeRange(0, attributedText.length)
429+
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
430+
usingBlock:^(NSParagraphStyle *style, __unused NSRange range, __unused BOOL *stop) {
431+
if (style && style.maximumLineHeight > 0) {
432+
maxLineHeight = MAX(maxLineHeight, style.maximumLineHeight);
433+
}
434+
}];
435+
436+
if (maxLineHeight == 0) {
437+
return;
438+
}
439+
440+
[attributedText enumerateAttribute:NSFontAttributeName
441+
inRange:NSMakeRange(0, attributedText.length)
442+
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
443+
usingBlock:^(UIFont *font, __unused NSRange range, __unused BOOL *stop) {
444+
if (font) {
445+
maxFontLineHeight = MAX(maxFontLineHeight, font.lineHeight);
446+
}
447+
}];
448+
449+
if (maxLineHeight >= maxFontLineHeight) {
450+
return;
451+
}
452+
453+
_glyphOverflow = ceil((maxFontLineHeight - maxLineHeight) / 2.0);
385454
}
386455

387456
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
@@ -406,6 +475,13 @@ - (void)drawRect:(CGRect)rect
406475

407476
CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
408477

478+
if (_glyphOverflow > 0) {
479+
// Offset origin to account for the expanded text view, and expand height
480+
// so the NSTextContainer has room for full glyph rendering.
481+
frame.origin.y += _glyphOverflow;
482+
frame.size.height += _glyphOverflow * 2;
483+
}
484+
409485
[nativeTextLayoutManager drawAttributedString:stateData.attributedString
410486
paragraphAttributes:_paragraphAttributes
411487
frame:frame
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Text, View, StyleSheet, SafeAreaView } from 'react-native';
2+
3+
export function LineHeightTest() {
4+
return (
5+
<SafeAreaView style={styles.container}>
6+
<Text style={styles.paragraph}>
7+
The view will not be aligned
8+
{'\n'}No way this text is aligned with the view
9+
</Text>
10+
</SafeAreaView>
11+
);
12+
}
13+
14+
const styles = StyleSheet.create({
15+
container: {
16+
padding: 36,
17+
},
18+
paragraph: {
19+
fontSize: 14,
20+
fontWeight: 'bold',
21+
textAlign: 'center',
22+
lineHeight: 10,
23+
overflow: 'visible',
24+
},
25+
});

packages/rn-tester/js/RNTesterAppShared.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
useColorScheme,
4242
useWindowDimensions,
4343
} from 'react-native';
44+
import { LineHeightTest } from './LineHeightTest';
4445

4546
// In Bridgeless mode, in dev, enable static view config validator
4647
if (global.RN$Bridgeless === true && __DEV__) {
@@ -274,6 +275,9 @@ const RNTesterApp = ({
274275
// Hide chrome if we don't have much screen space and are showing UI for tests
275276
const shouldHideChrome = isScreenTiny && hadDeepLink;
276277

278+
return (
279+
<LineHeightTest />
280+
)
277281
return (
278282
<RNTesterThemeContext.Provider value={theme}>
279283
{Platform.OS === 'android' ? (

0 commit comments

Comments
 (0)