From 70d774f3341dabbdaaf5afc82563d3a0a68ea35b Mon Sep 17 00:00:00 2001 From: Andrew Kunkel Date: Tue, 16 Dec 2025 13:23:13 -0800 Subject: [PATCH 1/2] Claude fix for stroke positioning in iOS. --- .../Libraries/Text/Text/RCTTextShadowView.mm | 53 +++++++------------ .../Libraries/Text/Text/RCTTextView.mm | 44 ++++++++------- 2 files changed, 42 insertions(+), 55 deletions(-) diff --git a/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm b/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm index 0b34a8629bd257..90b9fbd48fdf96 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm +++ b/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm @@ -405,28 +405,13 @@ - (CGFloat)lastBaselineForSize:(CGSize)size [attributedText enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, attributedText.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired - usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) { - if (maximumDescender > font.descender) { - maximumDescender = font.descender; - } - }]; - - // Account for stroke width in baseline calculation - __block CGFloat strokeWidth = 0; - [attributedText enumerateAttribute:@"RCTTextStrokeWidth" - inRange:NSMakeRange(0, attributedText.length) - options:0 - usingBlock:^(id value, NSRange range, BOOL *stop) { - if (value && [value isKindOfClass:[NSNumber class]]) { - CGFloat width = [value floatValue]; - if (width > 0) { - strokeWidth = MAX(strokeWidth, width); - *stop = YES; - } - } - }]; + usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) { + if (maximumDescender > font.descender) { + maximumDescender = font.descender; + } + }]; - return size.height + maximumDescender + strokeWidth; + return size.height + maximumDescender; } static YGSize RCTTextShadowViewMeasure( @@ -463,29 +448,27 @@ static YGSize RCTTextShadowViewMeasure( inRange:NSMakeRange(0, textStorage.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { - if (value && [value isKindOfClass:[NSNumber class]]) { - CGFloat width = [value floatValue]; - if (width > 0) { - strokeWidth = MAX(strokeWidth, width); - *stop = YES; - } - } - }]; + if (value && [value isKindOfClass:[NSNumber class]]) { + CGFloat width = [value floatValue]; + if (width > 0) { + strokeWidth = MAX(strokeWidth, width); + *stop = YES; + } + } + }]; if (strokeWidth > 0) { size.width += strokeWidth; - size.height += strokeWidth; } - size = (CGSize){ - MIN(RCTCeilPixelValue(size.width), maximumSize.width), MIN(RCTCeilPixelValue(size.height), maximumSize.height)}; + size = (CGSize){MIN(RCTCeilPixelValue(size.width), maximumSize.width), + MIN(RCTCeilPixelValue(size.height), maximumSize.height)}; // Adding epsilon value illuminates problems with converting values from // `double` to `float`, and then rounding them to pixel grid in Yoga. CGFloat epsilon = 0.001; - return (YGSize){ - RCTYogaFloatFromCoreGraphicsFloat(size.width + epsilon), - RCTYogaFloatFromCoreGraphicsFloat(size.height + epsilon)}; + return (YGSize){RCTYogaFloatFromCoreGraphicsFloat(size.width + epsilon), + RCTYogaFloatFromCoreGraphicsFloat(size.height + epsilon)}; } static float RCTTextShadowViewBaseline(YGNodeConstRef node, const float width, const float height) diff --git a/packages/react-native/Libraries/Text/Text/RCTTextView.mm b/packages/react-native/Libraries/Text/Text/RCTTextView.mm index 0d92ec3dee0ff1..aa72833cb877b6 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextView.mm +++ b/packages/react-native/Libraries/Text/Text/RCTTextView.mm @@ -131,21 +131,23 @@ - (void)drawRect:(CGRect)rect inRange:characterRange options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { - if (value && [value isKindOfClass:[NSNumber class]]) { - CGFloat width = [value floatValue]; - if (width > 0) { - hasStroke = YES; - strokeWidth = width; - strokeColor = [_textStorage attribute:@"RCTTextStrokeColor" atIndex:range.location effectiveRange:NULL]; - - if (strokeColor) { - CGFloat r, g, b, a; - [strokeColor getRed:&r green:&g blue:&b alpha:&a]; - } - *stop = YES; - } - } - }]; + if (value && [value isKindOfClass:[NSNumber class]]) { + CGFloat width = [value floatValue]; + if (width > 0) { + hasStroke = YES; + strokeWidth = width; + strokeColor = [_textStorage attribute:@"RCTTextStrokeColor" + atIndex:range.location + effectiveRange:NULL]; + + if (strokeColor) { + CGFloat r, g, b, a; + [strokeColor getRed:&r green:&g blue:&b alpha:&a]; + } + *stop = YES; + } + } + }]; if (hasStroke && strokeColor) { CGContextRef context = UIGraphicsGetCurrentContext(); @@ -157,16 +159,17 @@ - (void)drawRect:(CGRect)rect CGFloat strokeInset = strokeWidth / 2; // PASS 1: Draw stroke outline + // Note: Only offset X by strokeInset, not Y, to match Android behavior + // This ensures consistent vertical spacing when stroke is applied CGContextSaveGState(context); CGContextSetTextDrawingMode(context, kCGTextStroke); NSMutableAttributedString *strokeText = [_textStorage mutableCopy]; - [strokeText addAttribute:NSForegroundColorAttributeName - value:strokeColor - range:characterRange]; + [strokeText addAttribute:NSForegroundColorAttributeName value:strokeColor range:characterRange]; CGContextSetTextMatrix(context, CGAffineTransformIdentity); - CGContextTranslateCTM(context, _contentFrame.origin.x + strokeInset, self.bounds.size.height - _contentFrame.origin.y + strokeInset); + CGContextTranslateCTM( + context, _contentFrame.origin.x + strokeInset, self.bounds.size.height - _contentFrame.origin.y); CGContextScaleCTM(context, 1.0, -1.0); CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)strokeText); @@ -184,7 +187,8 @@ - (void)drawRect:(CGRect)rect CGContextSetTextDrawingMode(context, kCGTextFill); CGContextSetTextMatrix(context, CGAffineTransformIdentity); - CGContextTranslateCTM(context, _contentFrame.origin.x + strokeInset, self.bounds.size.height - _contentFrame.origin.y + strokeInset); + CGContextTranslateCTM( + context, _contentFrame.origin.x + strokeInset, self.bounds.size.height - _contentFrame.origin.y); CGContextScaleCTM(context, 1.0, -1.0); framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)_textStorage); From bee557870c264e4ebe59f029b19b7d375754d4fa Mon Sep 17 00:00:00 2001 From: Andrew Kunkel Date: Wed, 17 Dec 2025 10:43:17 -0800 Subject: [PATCH 2/2] Removed comment. --- packages/react-native/Libraries/Text/Text/RCTTextView.mm | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-native/Libraries/Text/Text/RCTTextView.mm b/packages/react-native/Libraries/Text/Text/RCTTextView.mm index aa72833cb877b6..a3f1640c4d6a16 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextView.mm +++ b/packages/react-native/Libraries/Text/Text/RCTTextView.mm @@ -159,8 +159,6 @@ - (void)drawRect:(CGRect)rect CGFloat strokeInset = strokeWidth / 2; // PASS 1: Draw stroke outline - // Note: Only offset X by strokeInset, not Y, to match Android behavior - // This ensures consistent vertical spacing when stroke is applied CGContextSaveGState(context); CGContextSetTextDrawingMode(context, kCGTextStroke);