From 5be76bce71010340107a67d1b682c830e84bbfc2 Mon Sep 17 00:00:00 2001 From: Shashikant Panchal Date: Thu, 27 Aug 2026 16:36:18 +0530 Subject: [PATCH 1/2] fix(ios): prevent missing hairlineWidth border strokes at subpixel Y offsets (#58054) --- .../Mounting/ComponentViews/View/RCTViewComponentView.mm | 4 +++- packages/react-native/React/Views/RCTBorderDrawing.m | 8 ++------ packages/react-native/React/Views/RCTView.m | 5 ++++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 37db047e8b25..1fbd7a9ac482 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -887,7 +887,9 @@ static void RCTAddContourEffectToLayer( UIEdgeInsets imageCapInsets = image.capInsets; CGRect contentsCenter = CGRect{ CGPoint{imageCapInsets.left / imageSize.width, imageCapInsets.top / imageSize.height}, - CGSize{(CGFloat)1.0 / imageSize.width, (CGFloat)1.0 / imageSize.height}}; + CGSize{ + MAX(0, (imageSize.width - imageCapInsets.left - imageCapInsets.right) / imageSize.width), + MAX(0, (imageSize.height - imageCapInsets.top - imageCapInsets.bottom) / imageSize.height)}}; layer.contents = (id)image.CGImage; layer.contentsScale = image.scale; diff --git a/packages/react-native/React/Views/RCTBorderDrawing.m b/packages/react-native/React/Views/RCTBorderDrawing.m index 472885ca0c39..41f1297b67d6 100644 --- a/packages/react-native/React/Views/RCTBorderDrawing.m +++ b/packages/react-native/React/Views/RCTBorderDrawing.m @@ -247,12 +247,8 @@ static CGPathRef RCTPathCreateOuterOutline(BOOL drawToEdge, CGRect rect, RCTCorn const CGSize size = makeStretchable ? (CGSize){ // 1pt for the middle stretchable area along each axis - // we also need to round the edge insets to avoid border bleeding - // this is because if the size is decimal, when calculating the unit - // rectangle for CALayer.contentsCenter we encounter rounding errors - // which causes visual glitches - ceil(edgeInsets.left) + 1 + ceil(edgeInsets.right), - ceil(edgeInsets.top) + 1 + ceil(edgeInsets.bottom), + edgeInsets.left + 1 + edgeInsets.right, + edgeInsets.top + 1 + edgeInsets.bottom, } : viewSize; UIGraphicsImageRenderer *const imageRenderer = diff --git a/packages/react-native/React/Views/RCTView.m b/packages/react-native/React/Views/RCTView.m index bc39a7212f7b..614851309cb7 100644 --- a/packages/react-native/React/Views/RCTView.m +++ b/packages/react-native/React/Views/RCTView.m @@ -860,7 +860,10 @@ - (void)displayLayer:(CALayer *)layer CGSize size = image.size; UIEdgeInsets insets = image.capInsets; CGRectMake( - insets.left / size.width, insets.top / size.height, (CGFloat)1.0 / size.width, (CGFloat)1.0 / size.height); + insets.left / size.width, + insets.top / size.height, + MAX(0, (size.width - insets.left - insets.right) / size.width), + MAX(0, (size.height - insets.top - insets.bottom) / size.height)); }); layer.contents = (id)image.CGImage; From d28e05cb49db62e07bd270ae10df24631777f1d8 Mon Sep 17 00:00:00 2001 From: Shashikant Panchal Date: Thu, 10 Sep 2026 11:02:27 +0530 Subject: [PATCH 2/2] test(rn-tester): add hairline border test at subpixel Y offsets --- .../js/examples/Border/BorderExample.js | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/packages/rn-tester/js/examples/Border/BorderExample.js b/packages/rn-tester/js/examples/Border/BorderExample.js index 6b36f32e4432..4e0d9251f911 100644 --- a/packages/rn-tester/js/examples/Border/BorderExample.js +++ b/packages/rn-tester/js/examples/Border/BorderExample.js @@ -13,6 +13,7 @@ import type {RNTesterModule} from '../../types/RNTesterTypes'; import hotdog from '../../assets/hotdog.jpg'; +import RNTesterText from '../../components/RNTesterText'; import * as React from 'react'; import { DynamicColorIOS, @@ -257,6 +258,33 @@ const styles = StyleSheet.create({ top: -10, backgroundColor: 'blue', }, + hairlineBox: { + width: 44, + height: 44, + borderWidth: StyleSheet.hairlineWidth, + borderColor: 'black', + backgroundColor: '#f5f5f5', + }, + hairlineBoxContainer: { + alignItems: 'center', + marginRight: 8, + }, + hairlineRow: { + borderBottomWidth: StyleSheet.hairlineWidth, + borderColor: 'black', + justifyContent: 'center', + paddingHorizontal: 4, + }, + subpixelLabel: { + fontSize: 9, + marginTop: 4, + }, + sectionTitle: { + fontSize: 11, + fontWeight: 'bold', + marginBottom: 6, + marginTop: 10, + }, }); export default { @@ -622,5 +650,108 @@ export default { ); }, }, + { + title: 'Hairline borders at subpixel Y offsets', + name: 'hairline-subpixel-y-offsets', + description: + 'Ensure hairlineWidth borders render completely on all sides even when positioned at fractional/subpixel Y offsets on iOS', + render: function (): React.Node { + const subpixelYOffsets = [0, 0.25, 0.33, 0.5, 0.67, 0.75]; + return ( + + + Boxes at fractional Y offsets (borderWidth = hairlineWidth): + + + {subpixelYOffsets.map(yOffset => ( + + + + {`+${yOffset}pt`} + + + ))} + + + + Stacked rows with hairline bottom borders (fractional heights): + + + {subpixelYOffsets.map((fraction, index) => ( + + + {`Row ${index + 1} (height: ${(24 + fraction).toFixed(2)}pt)`} + + + ))} + + + + Single-side hairline borders at fractional Y offset (+0.33pt): + + + + + top + + + + bottom + + + + left + + + + right + + + + ); + }, + }, ], } as RNTesterModule;