Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Improve text line height calculation to properly align text on iOS #46884

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
48 changes: 46 additions & 2 deletions packages/react-native/Libraries/Text/Text/RCTTextView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#import <React/RCTUtils.h>
#import <React/UIView+React.h>
#import <react/featureflags/ReactNativeFeatureFlags.h>

#import <React/RCTTextShadowView.h>

Expand Down Expand Up @@ -98,6 +99,42 @@ - (void)setTextStorage:(NSTextStorage *)textStorage
[self setNeedsDisplay];
}

- (CGPoint)calculateDrawingPointWithTextStorage:(NSTextStorage *)textStorage
contentFrame:(CGRect)contentFrame {
if ([textStorage length] == 0) {
return contentFrame.origin;
}

UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL];
if (!font) {
font = [UIFont systemFontOfSize:14];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn’t find systemFontSize used anywhere else in the repo, and the default font size of 14 has been hardcoded. Also, since systemFontSize is not available on tvOS, I’m not sure if it would be a suitable replacement. Let me know your thoughts on whether we should stick with the hardcoded value for consistency or if there’s a preferred approach.

}

NSParagraphStyle *paragraphStyle = [textStorage attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:NULL];

CGFloat lineHeight = font.lineHeight;
if (paragraphStyle && paragraphStyle.minimumLineHeight > 0) {
lineHeight = paragraphStyle.minimumLineHeight;
}

CGFloat ascent = font.ascender;
CGFloat descent = fabs(font.descender);
CGFloat textHeight = ascent + descent;

CGFloat verticalOffset = 0;
// Adjust vertical offset to ensure text is vertically centered relative to the line height.
// Positive offset when text height exceeds line height, negative when line height exceeds text height.
if (textHeight > lineHeight) {
CGFloat difference = textHeight - lineHeight;
verticalOffset = difference / 2.0;
} else if (textHeight < lineHeight) {
CGFloat difference = lineHeight - textHeight;
verticalOffset = -(difference / 2.0);
}

return CGPointMake(contentFrame.origin.x, contentFrame.origin.y + verticalOffset);
}

- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
Expand All @@ -118,8 +155,15 @@ - (void)drawRect:(CGRect)rect
#endif

NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:_contentFrame.origin];
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:_contentFrame.origin];

if (facebook::react::ReactNativeFeatureFlags::enableLineHeightCenteringOnIOS()) {
CGPoint drawingPoint = [self calculateDrawingPointWithTextStorage:_textStorage contentFrame:_contentFrame];
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:drawingPoint];
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:drawingPoint];
} else {
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:_contentFrame.origin];
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:_contentFrame.origin];
}

__block UIBezierPath *highlightPath = nil;
NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#import "RCTParagraphComponentAccessibilityProvider.h"

#import <MobileCoreServices/UTCoreTypes.h>
#import <react/featureflags/ReactNativeFeatureFlags.h>
#import <react/renderer/components/text/ParagraphComponentDescriptor.h>
#import <react/renderer/components/text/ParagraphProps.h>
#import <react/renderer/components/text/ParagraphState.h>
Expand Down Expand Up @@ -326,6 +327,40 @@ @implementation RCTParagraphTextView {
CAShapeLayer *_highlightLayer;
}

- (CGRect)calculateCenteredFrameWithAttributedText:(NSAttributedString *)attributedText
frame:(CGRect)frame {
UIFont *font = [attributedText attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL];
if (!font) {
font = [UIFont systemFontOfSize:14];
}

NSParagraphStyle *paragraphStyle = [attributedText attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:NULL];
CGFloat lineHeight = font.lineHeight;

if (paragraphStyle && paragraphStyle.minimumLineHeight > 0) {
lineHeight = paragraphStyle.minimumLineHeight;
}

CGFloat ascent = font.ascender;
CGFloat descent = fabs(font.descender);
CGFloat textHeight = ascent + descent;

CGFloat verticalOffset = 0;
// Adjust vertical offset to ensure text is vertically centered relative to the line height.
// Positive offset when text height exceeds line height, negative when line height exceeds text height.
if (textHeight > lineHeight) {
CGFloat difference = textHeight - lineHeight;
verticalOffset = difference / 2.0;
} else if (textHeight < lineHeight) {
CGFloat difference = lineHeight - textHeight;
verticalOffset = -(difference / 2.0);
}

frame.origin.y += verticalOffset;

return frame;
}

- (void)drawRect:(CGRect)rect
{
if (!_state) {
Expand All @@ -343,6 +378,11 @@ - (void)drawRect:(CGRect)rect

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

if (ReactNativeFeatureFlags::enableLineHeightCenteringOnIOS()) {
NSAttributedString *attributedText = RCTNSAttributedStringFromAttributedString(_state->getData().attributedString);
frame = [self calculateCenteredFrameWithAttributedText:attributedText frame:frame];
}

[nativeTextLayoutManager drawAttributedString:_state->getData().attributedString
paragraphAttributes:_paragraphAttributes
frame:frame
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<e568acc70be10bdd8f86caddd03cde05>>
* @generated SignedSource<<bbb35582fcb449903f58a5d4018755d9>>
*/

/**
Expand Down Expand Up @@ -58,12 +58,6 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun enableAlignItemsBaselineOnFabricIOS(): Boolean = accessor.enableAlignItemsBaselineOnFabricIOS()

/**
* When enabled, custom line height calculation will be centered from top to bottom.
*/
@JvmStatic
public fun enableAndroidLineHeightCentering(): Boolean = accessor.enableAndroidLineHeightCentering()

/**
* Feature flag to enable the new bridgeless architecture. Note: Enabling this will force enable the following flags: `useTurboModules` & `enableFabricRenderer.
*/
Expand Down Expand Up @@ -136,6 +130,18 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun enableLayoutAnimationsOnIOS(): Boolean = accessor.enableLayoutAnimationsOnIOS()

/**
* When enabled, custom line height calculation will be centered from top to bottom.
*/
@JvmStatic
public fun enableLineHeightCenteringOnAndroid(): Boolean = accessor.enableLineHeightCenteringOnAndroid()

/**
* When enabled, custom line height calculation will be centered from top to bottom.
*/
@JvmStatic
public fun enableLineHeightCenteringOnIOS(): Boolean = accessor.enableLineHeightCenteringOnIOS()

/**
* Enables the reporting of long tasks through `PerformanceObserver`. Only works if the event loop is enabled.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<a2a7b39f3f71be2a278faf2c21ede6a3>>
* @generated SignedSource<<02f9fa1fd92a89a823e8fd35e1a2409b>>
*/

/**
Expand All @@ -25,7 +25,6 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso
private var batchRenderingUpdatesInEventLoopCache: Boolean? = null
private var completeReactInstanceCreationOnBgThreadOnAndroidCache: Boolean? = null
private var enableAlignItemsBaselineOnFabricIOSCache: Boolean? = null
private var enableAndroidLineHeightCenteringCache: Boolean? = null
private var enableBridgelessArchitectureCache: Boolean? = null
private var enableCleanTextInputYogaNodeCache: Boolean? = null
private var enableDeletionOfUnmountedViewsCache: Boolean? = null
Expand All @@ -38,6 +37,8 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
private var enableLayoutAnimationsOnAndroidCache: Boolean? = null
private var enableLayoutAnimationsOnIOSCache: Boolean? = null
private var enableLineHeightCenteringOnAndroidCache: Boolean? = null
private var enableLineHeightCenteringOnIOSCache: Boolean? = null
private var enableLongTaskAPICache: Boolean? = null
private var enableMicrotasksCache: Boolean? = null
private var enablePreciseSchedulingForPremountItemsOnAndroidCache: Boolean? = null
Expand Down Expand Up @@ -115,15 +116,6 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso
return cached
}

override fun enableAndroidLineHeightCentering(): Boolean {
var cached = enableAndroidLineHeightCenteringCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.enableAndroidLineHeightCentering()
enableAndroidLineHeightCenteringCache = cached
}
return cached
}

override fun enableBridgelessArchitecture(): Boolean {
var cached = enableBridgelessArchitectureCache
if (cached == null) {
Expand Down Expand Up @@ -232,6 +224,24 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso
return cached
}

override fun enableLineHeightCenteringOnAndroid(): Boolean {
var cached = enableLineHeightCenteringOnAndroidCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.enableLineHeightCenteringOnAndroid()
enableLineHeightCenteringOnAndroidCache = cached
}
return cached
}

override fun enableLineHeightCenteringOnIOS(): Boolean {
var cached = enableLineHeightCenteringOnIOSCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.enableLineHeightCenteringOnIOS()
enableLineHeightCenteringOnIOSCache = cached
}
return cached
}

override fun enableLongTaskAPI(): Boolean {
var cached = enableLongTaskAPICache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<3cf8639dfd8a92a954a055c3ebdc749c>>
* @generated SignedSource<<fde4302e82485beb6a4f7eb9cc5612aa>>
*/

/**
Expand Down Expand Up @@ -38,8 +38,6 @@ public object ReactNativeFeatureFlagsCxxInterop {

@DoNotStrip @JvmStatic public external fun enableAlignItemsBaselineOnFabricIOS(): Boolean

@DoNotStrip @JvmStatic public external fun enableAndroidLineHeightCentering(): Boolean

@DoNotStrip @JvmStatic public external fun enableBridgelessArchitecture(): Boolean

@DoNotStrip @JvmStatic public external fun enableCleanTextInputYogaNode(): Boolean
Expand All @@ -64,6 +62,10 @@ public object ReactNativeFeatureFlagsCxxInterop {

@DoNotStrip @JvmStatic public external fun enableLayoutAnimationsOnIOS(): Boolean

@DoNotStrip @JvmStatic public external fun enableLineHeightCenteringOnAndroid(): Boolean

@DoNotStrip @JvmStatic public external fun enableLineHeightCenteringOnIOS(): Boolean

@DoNotStrip @JvmStatic public external fun enableLongTaskAPI(): Boolean

@DoNotStrip @JvmStatic public external fun enableMicrotasks(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<9e0db1a47596fec77c29122620b8f633>>
* @generated SignedSource<<af03c1fe9360349ef95a4939a8c014ee>>
*/

/**
Expand Down Expand Up @@ -33,8 +33,6 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi

override fun enableAlignItemsBaselineOnFabricIOS(): Boolean = true

override fun enableAndroidLineHeightCentering(): Boolean = false

override fun enableBridgelessArchitecture(): Boolean = false

override fun enableCleanTextInputYogaNode(): Boolean = false
Expand All @@ -59,6 +57,10 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi

override fun enableLayoutAnimationsOnIOS(): Boolean = true

override fun enableLineHeightCenteringOnAndroid(): Boolean = false

override fun enableLineHeightCenteringOnIOS(): Boolean = false

override fun enableLongTaskAPI(): Boolean = false

override fun enableMicrotasks(): Boolean = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<91c4268e7e88e2e3c1f3d50d149c0d2b>>
* @generated SignedSource<<f0f8c57691724d5ace09b10df2afa890>>
*/

/**
Expand All @@ -29,7 +29,6 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces
private var batchRenderingUpdatesInEventLoopCache: Boolean? = null
private var completeReactInstanceCreationOnBgThreadOnAndroidCache: Boolean? = null
private var enableAlignItemsBaselineOnFabricIOSCache: Boolean? = null
private var enableAndroidLineHeightCenteringCache: Boolean? = null
private var enableBridgelessArchitectureCache: Boolean? = null
private var enableCleanTextInputYogaNodeCache: Boolean? = null
private var enableDeletionOfUnmountedViewsCache: Boolean? = null
Expand All @@ -42,6 +41,8 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
private var enableLayoutAnimationsOnAndroidCache: Boolean? = null
private var enableLayoutAnimationsOnIOSCache: Boolean? = null
private var enableLineHeightCenteringOnAndroidCache: Boolean? = null
private var enableLineHeightCenteringOnIOSCache: Boolean? = null
private var enableLongTaskAPICache: Boolean? = null
private var enableMicrotasksCache: Boolean? = null
private var enablePreciseSchedulingForPremountItemsOnAndroidCache: Boolean? = null
Expand Down Expand Up @@ -124,16 +125,6 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces
return cached
}

override fun enableAndroidLineHeightCentering(): Boolean {
var cached = enableAndroidLineHeightCenteringCache
if (cached == null) {
cached = currentProvider.enableAndroidLineHeightCentering()
accessedFeatureFlags.add("enableAndroidLineHeightCentering")
enableAndroidLineHeightCenteringCache = cached
}
return cached
}

override fun enableBridgelessArchitecture(): Boolean {
var cached = enableBridgelessArchitectureCache
if (cached == null) {
Expand Down Expand Up @@ -254,6 +245,26 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces
return cached
}

override fun enableLineHeightCenteringOnAndroid(): Boolean {
var cached = enableLineHeightCenteringOnAndroidCache
if (cached == null) {
cached = currentProvider.enableLineHeightCenteringOnAndroid()
accessedFeatureFlags.add("enableLineHeightCenteringOnAndroid")
enableLineHeightCenteringOnAndroidCache = cached
}
return cached
}

override fun enableLineHeightCenteringOnIOS(): Boolean {
var cached = enableLineHeightCenteringOnIOSCache
if (cached == null) {
cached = currentProvider.enableLineHeightCenteringOnIOS()
accessedFeatureFlags.add("enableLineHeightCenteringOnIOS")
enableLineHeightCenteringOnIOSCache = cached
}
return cached
}

override fun enableLongTaskAPI(): Boolean {
var cached = enableLongTaskAPICache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<193bb7803261004003d9009b44810c2c>>
* @generated SignedSource<<95ff497c335cac627d85e166d8178558>>
*/

/**
Expand Down Expand Up @@ -33,8 +33,6 @@ public interface ReactNativeFeatureFlagsProvider {

@DoNotStrip public fun enableAlignItemsBaselineOnFabricIOS(): Boolean

@DoNotStrip public fun enableAndroidLineHeightCentering(): Boolean

@DoNotStrip public fun enableBridgelessArchitecture(): Boolean

@DoNotStrip public fun enableCleanTextInputYogaNode(): Boolean
Expand All @@ -59,6 +57,10 @@ public interface ReactNativeFeatureFlagsProvider {

@DoNotStrip public fun enableLayoutAnimationsOnIOS(): Boolean

@DoNotStrip public fun enableLineHeightCenteringOnAndroid(): Boolean

@DoNotStrip public fun enableLineHeightCenteringOnIOS(): Boolean

@DoNotStrip public fun enableLongTaskAPI(): Boolean

@DoNotStrip public fun enableMicrotasks(): Boolean
Expand Down
Loading