From f256ec93959bd63b4fd409715dce477fdeb3c8cd Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 6 Mar 2026 14:17:25 +0100 Subject: [PATCH 01/14] feat: add borderRadius to nested text --- ...0.83.1+031+nested-text-border-radius.patch | 406 ++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 patches/react-native/react-native+0.83.1+031+nested-text-border-radius.patch diff --git a/patches/react-native/react-native+0.83.1+031+nested-text-border-radius.patch b/patches/react-native/react-native+0.83.1+031+nested-text-border-radius.patch new file mode 100644 index 000000000000..8f78622f9808 --- /dev/null +++ b/patches/react-native/react-native+0.83.1+031+nested-text-border-radius.patch @@ -0,0 +1,406 @@ +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +index 906dfbc..0fb3932 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +@@ -52,6 +52,7 @@ import com.facebook.react.uimanager.style.BorderRadiusProp; + import com.facebook.react.uimanager.style.BorderStyle; + import com.facebook.react.uimanager.style.LogicalEdge; + import com.facebook.react.uimanager.style.Overflow; ++import com.facebook.react.views.text.internal.span.DrawCommandSpan; + import com.facebook.react.views.text.internal.span.ReactTagSpan; + import com.facebook.react.views.text.internal.span.TextInlineImageSpan; + import com.facebook.react.views.text.internal.span.TextInlineViewPlaceholderSpan; +@@ -364,6 +365,24 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie + BackgroundStyleApplicator.clipToPaddingBox(this, canvas); + } + ++ // Draw custom DrawCommandSpan backgrounds before the text so they appear behind it. ++ // PreparedLayoutTextView handles this natively, but ReactTextView (standard TextView) ++ // does not know about DrawCommandSpan, so we invoke onPreDraw manually here. ++ Layout layout = getLayout(); ++ if (spanned != null && layout != null) { ++ DrawCommandSpan[] drawSpans = ++ spanned.getSpans(0, spanned.length(), DrawCommandSpan.class); ++ if (drawSpans != null) { ++ int saveCount = canvas.save(); ++ canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop()); ++ for (DrawCommandSpan span : drawSpans) { ++ span.onPreDraw( ++ spanned.getSpanStart(span), spanned.getSpanEnd(span), canvas, layout); ++ } ++ canvas.restoreToCount(saveCount); ++ } ++ } ++ + super.onDraw(canvas); + } + } +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt +index 433afa5..b36447c 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt +@@ -68,6 +68,9 @@ public class TextAttributeProps private constructor() { + public var isBackgroundColorSet: Boolean = false + private set + ++ public var borderRadius: Float = Float.NaN ++ private set ++ + public var opacity: Float = Float.NaN + private set + +@@ -376,6 +379,7 @@ public class TextAttributeProps private constructor() { + public const val TA_KEY_ROLE: Int = 26 + public const val TA_KEY_TEXT_TRANSFORM: Int = 27 + public const val TA_KEY_MAX_FONT_SIZE_MULTIPLIER: Int = 29 ++ public const val TA_KEY_BORDER_RADIUS: Int = 30 + + public const val UNSET: Int = -1 + +@@ -430,6 +434,7 @@ public class TextAttributeProps private constructor() { + TA_KEY_TEXT_TRANSFORM -> result.setTextTransform(entry.stringValue) + TA_KEY_MAX_FONT_SIZE_MULTIPLIER -> + result.maxFontSizeMultiplier = entry.doubleValue.toFloat() ++ TA_KEY_BORDER_RADIUS -> result.borderRadius = entry.doubleValue.toFloat() + } + } + +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt +index 5a893fd..a8b8961 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt +@@ -40,6 +40,7 @@ import com.facebook.react.views.text.internal.span.CustomLineHeightSpan + import com.facebook.react.views.text.internal.span.CustomStyleSpan + import com.facebook.react.views.text.internal.span.ReactAbsoluteSizeSpan + import com.facebook.react.views.text.internal.span.ReactBackgroundColorSpan ++import com.facebook.react.views.text.internal.span.ReactBackgroundDrawSpan + import com.facebook.react.views.text.internal.span.ReactClickableSpan + import com.facebook.react.views.text.internal.span.ReactForegroundColorSpan + import com.facebook.react.views.text.internal.span.ReactFragmentIndexSpan +@@ -269,7 +270,13 @@ internal object TextLayoutManager { + } + if (textAttributes.isBackgroundColorSet) { + textAttributes.backgroundColor +- ?.let { ReactBackgroundColorSpan(it) } ++ ?.let { ++ if (!textAttributes.borderRadius.isNaN()) { ++ ReactBackgroundDrawSpan(it, PixelUtil.toPixelFromDIP(textAttributes.borderRadius)) ++ } else { ++ ReactBackgroundColorSpan(it) ++ } ++ } + ?.let { SetSpanOperation(start, end, it) } + ?.let { ops.add(it) } + } +@@ -438,12 +445,16 @@ internal object TextLayoutManager { + } + + if (fragment.props.isBackgroundColorSet) { +- spannable.setSpan( +- fragment.props.backgroundColor?.let { ReactBackgroundColorSpan(it) }, +- start, +- end, +- spanFlags, +- ) ++ val bgSpan = ++ fragment.props.backgroundColor?.let { ++ if (!fragment.props.borderRadius.isNaN()) { ++ ReactBackgroundDrawSpan( ++ it, PixelUtil.toPixelFromDIP(fragment.props.borderRadius)) ++ } else { ++ ReactBackgroundColorSpan(it) ++ } ++ } ++ spannable.setSpan(bgSpan, start, end, spanFlags) + } + + if (!fragment.props.opacity.isNaN()) { +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/DrawCommandSpan.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/DrawCommandSpan.kt +new file mode 100644 +index 0000000..b9d2591 +--- /dev/null ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/DrawCommandSpan.kt +@@ -0,0 +1,28 @@ ++/* ++ * Copyright (c) Meta Platforms, Inc. and affiliates. ++ * ++ * This source code is licensed under the MIT license found in the ++ * LICENSE file in the root directory of this source tree. ++ */ ++ ++package com.facebook.react.views.text.internal.span ++ ++import android.graphics.Canvas ++import android.text.Layout ++import android.text.style.UpdateAppearance ++ ++/** ++ * May be overriden to implement charater styles which are applied by [PreparedLayoutTextView] ++ * during the drawing of text, against the underlying Android canvas ++ */ ++public abstract class DrawCommandSpan : UpdateAppearance, ReactSpan { ++ /** ++ * Called before the text is drawn. This happens after the Paragraph component has drawn its ++ * background, but may be called before text spans with their own background color are drawn. ++ */ ++ public open fun onPreDraw(start: Int, end: Int, canvas: Canvas, layout: Layout): Unit = Unit ++ ++ /** Called after the text is drawn, including some effects like text shadows */ ++ public open fun onDraw(start: Int, end: Int, canvas: Canvas, layout: Layout): Unit = Unit ++} +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactBackgroundDrawSpan.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactBackgroundDrawSpan.kt +new file mode 100644 +index 0000000..000b569 +--- /dev/null ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactBackgroundDrawSpan.kt +@@ -0,0 +1,124 @@ ++/* Copyright (c) Meta Platforms, Inc. and affiliates. ++ * ++ * This source code is licensed under the MIT license found in the ++ * LICENSE file in the root directory of this source tree. ++ */ ++ ++package com.facebook.react.views.text.internal.span ++ ++import android.graphics.Canvas ++import android.graphics.Paint ++import android.graphics.Path ++import android.graphics.RectF ++import android.text.Layout ++import androidx.annotation.ColorInt ++ ++/** ++ * A [DrawCommandSpan] that draws a rounded-rectangle background behind inline text spans. When ++ * borderRadius is 0, this draws a plain rectangle identical to [ReactBackgroundColorSpan]. ++ * ++ * For multiline spans, only the outer corners are rounded: left corners on the first line, right ++ * corners on the last line (flipped for RTL). ++ */ ++internal class ReactBackgroundDrawSpan( ++ @ColorInt private val backgroundColor: Int, ++ private val borderRadius: Float, ++) : DrawCommandSpan() { ++ ++ private val paint = ++ Paint().apply { ++ color = backgroundColor ++ isAntiAlias = true ++ style = Paint.Style.FILL ++ } ++ ++ private val rectF = RectF() ++ private val path = Path() ++ ++ override fun onPreDraw(start: Int, end: Int, canvas: Canvas, layout: Layout) { ++ if (start >= end) return ++ ++ val startLine = layout.getLineForOffset(start) ++ val endLine = layout.getLineForOffset(end) ++ ++ for (line in startLine..endLine) { ++ val lineStart = layout.getLineStart(line) ++ val lineEnd = layout.getLineEnd(line) ++ ++ // When the span starts at or before this line, use the line's left edge. ++ // Otherwise use the actual character position of the span start. ++ val left = ++ if (start <= lineStart) { ++ layout.getLineLeft(line) ++ } else { ++ layout.getPrimaryHorizontal(start) ++ } ++ ++ // When the span extends past this line, use the line's right edge. ++ // getLineEnd() returns the offset of the first char on the NEXT line for ++ // soft-wrapped lines, so getPrimaryHorizontal(lineEnd) would incorrectly ++ // give us the left margin of the next line instead of the right edge. ++ val right = ++ if (end >= lineEnd) { ++ layout.getLineRight(line) ++ } else { ++ layout.getPrimaryHorizontal(end) ++ } ++ ++ val top = layout.getLineTop(line).toFloat() ++ val bottom = layout.getLineBottom(line).toFloat() ++ ++ val actualLeft = minOf(left, right) ++ val actualRight = maxOf(left, right) ++ ++ rectF.set(actualLeft, top, actualRight, bottom) ++ ++ if (borderRadius == 0f) { ++ canvas.drawRect(rectF, paint) ++ } else { ++ val isFirstLine = line == startLine ++ val isLastLine = line == endLine ++ val isSingleLine = isFirstLine && isLastLine ++ ++ if (isSingleLine) { ++ canvas.drawRoundRect(rectF, borderRadius, borderRadius, paint) ++ } else { ++ val rtl = layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT ++ drawSelectiveRoundRect(canvas, rectF, isFirstLine, isLastLine, rtl) ++ } ++ } ++ } ++ } ++ ++ private fun drawSelectiveRoundRect( ++ canvas: Canvas, ++ rect: RectF, ++ isFirstLine: Boolean, ++ isLastLine: Boolean, ++ rtl: Boolean, ++ ) { ++ // For LTR: first line rounds left corners, last line rounds right corners ++ // For RTL: first line rounds right corners, last line rounds left corners ++ val roundStart = isFirstLine ++ val roundEnd = isLastLine ++ val roundLeft = if (rtl) roundEnd else roundStart ++ val roundRight = if (rtl) roundStart else roundEnd ++ ++ val topLeft = if (roundLeft) borderRadius else 0f ++ val bottomLeft = if (roundLeft) borderRadius else 0f ++ val topRight = if (roundRight) borderRadius else 0f ++ val bottomRight = if (roundRight) borderRadius else 0f ++ ++ val radii = ++ floatArrayOf( ++ topLeft, topLeft, ++ topRight, topRight, ++ bottomRight, bottomRight, ++ bottomLeft, bottomLeft, ++ ) ++ ++ path.reset() ++ path.addRoundRect(rect, radii, Path.Direction.CW) ++ canvas.drawPath(path, paint) ++ } ++} +diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp +index 9cf89bf..3211ff8 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp +@@ -27,6 +27,9 @@ void TextAttributes::apply(TextAttributes textAttributes) { + : backgroundColor; + opacity = + !std::isnan(textAttributes.opacity) ? textAttributes.opacity : opacity; ++ borderRadius = textAttributes.borderRadius.has_value() ++ ? textAttributes.borderRadius ++ : borderRadius; + + // Font + fontFamily = !textAttributes.fontFamily.empty() ? textAttributes.fontFamily +@@ -141,7 +144,8 @@ bool TextAttributes::operator==(const TextAttributes& rhs) const { + layoutDirection, + accessibilityRole, + role, +- textTransform) == ++ textTransform, ++ borderRadius) == + std::tie( + rhs.foregroundColor, + rhs.backgroundColor, +@@ -164,7 +168,8 @@ bool TextAttributes::operator==(const TextAttributes& rhs) const { + rhs.layoutDirection, + rhs.accessibilityRole, + rhs.role, +- rhs.textTransform) && ++ rhs.textTransform, ++ rhs.borderRadius) && + floatEquality(maxFontSizeMultiplier, rhs.maxFontSizeMultiplier) && + floatEquality(opacity, rhs.opacity) && + floatEquality(fontSize, rhs.fontSize) && +@@ -199,6 +204,8 @@ SharedDebugStringConvertibleList TextAttributes::getDebugProps() const { + debugStringConvertibleItem( + "foregroundColor", foregroundColor, textAttributes.foregroundColor), + debugStringConvertibleItem("opacity", opacity, textAttributes.opacity), ++ debugStringConvertibleItem( ++ "borderRadius", borderRadius, textAttributes.borderRadius), + + // Font + debugStringConvertibleItem( +diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h +index b664524..ad40ac0 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h +@@ -42,6 +42,7 @@ class TextAttributes : public DebugStringConvertible { + SharedColor foregroundColor{}; + SharedColor backgroundColor{}; + Float opacity{std::numeric_limits::quiet_NaN()}; ++ std::optional borderRadius{}; + + // Font + std::string fontFamily{""}; +@@ -138,7 +139,8 @@ struct hash { + textAttributes.isPressable, + textAttributes.layoutDirection, + textAttributes.accessibilityRole, +- textAttributes.role); ++ textAttributes.role, ++ textAttributes.borderRadius); + } + }; + } // namespace std +diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +index f771236..a62da2e 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +@@ -1027,6 +1027,7 @@ constexpr static MapBuffer::Key TA_KEY_ROLE = 26; + constexpr static MapBuffer::Key TA_KEY_TEXT_TRANSFORM = 27; + constexpr static MapBuffer::Key TA_KEY_ALIGNMENT_VERTICAL = 28; + constexpr static MapBuffer::Key TA_KEY_MAX_FONT_SIZE_MULTIPLIER = 29; ++constexpr static MapBuffer::Key TA_KEY_BORDER_RADIUS = 30; + + // constants for ParagraphAttributes serialization + constexpr static MapBuffer::Key PA_KEY_MAX_NUMBER_OF_LINES = 0; +@@ -1171,6 +1172,9 @@ inline MapBuffer toMapBuffer(const TextAttributes &textAttributes) + if (textAttributes.role.has_value()) { + builder.putInt(TA_KEY_ROLE, static_cast(*textAttributes.role)); + } ++ if (textAttributes.borderRadius.has_value()) { ++ builder.putDouble(TA_KEY_BORDER_RADIUS, *textAttributes.borderRadius); ++ } + return builder.build(); + } + +diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp +index f20cd3c..35f21d1 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp +@@ -222,6 +222,12 @@ static TextAttributes convertRawProp( + "backgroundColor", + sourceTextAttributes.backgroundColor, + defaultTextAttributes.backgroundColor); ++ textAttributes.borderRadius = convertRawProp( ++ context, ++ rawProps, ++ "borderRadius", ++ sourceTextAttributes.borderRadius, ++ defaultTextAttributes.borderRadius); + + return textAttributes; + } +@@ -334,6 +340,8 @@ void BaseTextProps::setProp( + defaults, value, textAttributes, opacity, "opacity"); + REBUILD_FIELD_SWITCH_CASE( + defaults, value, textAttributes, backgroundColor, "backgroundColor"); ++ REBUILD_FIELD_SWITCH_CASE( ++ defaults, value, textAttributes, borderRadius, "borderRadius"); + } + } + +@@ -532,6 +540,12 @@ void BaseTextProps::appendTextAttributesProps( + oldProps->textAttributes.backgroundColor) { + result["backgroundColor"] = *textAttributes.backgroundColor; + } ++ ++ if (textAttributes.borderRadius != oldProps->textAttributes.borderRadius) { ++ result["borderRadius"] = textAttributes.borderRadius.has_value() ++ ? textAttributes.borderRadius.value() ++ : folly::dynamic(nullptr); ++ } + } + + #endif From efcb8417325c5143edcd9b6b4926fd6b81d18c25 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 6 Mar 2026 15:35:44 +0100 Subject: [PATCH 02/14] feat: add chat borderRadius to iOS --- ...0.83.1+031+nested-text-border-radius.patch | 162 +++++++++++++++++- 1 file changed, 161 insertions(+), 1 deletion(-) diff --git a/patches/react-native/react-native+0.83.1+031+nested-text-border-radius.patch b/patches/react-native/react-native+0.83.1+031+nested-text-border-radius.patch index 8f78622f9808..825bd643ee20 100644 --- a/patches/react-native/react-native+0.83.1+031+nested-text-border-radius.patch +++ b/patches/react-native/react-native+0.83.1+031+nested-text-border-radius.patch @@ -120,7 +120,7 @@ new file mode 100644 index 0000000..b9d2591 --- /dev/null +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/DrawCommandSpan.kt -@@ -0,0 +1,28 @@ +@@ -0,0 +1,27 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * @@ -404,3 +404,163 @@ index f20cd3c..35f21d1 100644 } #endif +diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h +index 902912e..26c04e9 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h +@@ -19,6 +19,9 @@ NSString *const RCTAttributedStringEventEmitterKey = @"EventEmitter"; + // String representation of either `role` or `accessibilityRole` + NSString *const RCTTextAttributesAccessibilityRoleAttributeName = @"AccessibilityRole"; + ++// Custom attribute for border radius on inline text backgrounds ++NSString *const RCTTextBorderRadiusAttributeName = @"RCTTextBorderRadius"; ++ + /* + * Creates `NSTextAttributes` from given `facebook::react::TextAttributes` + */ +diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +index f96a049..0e2c71d 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm ++++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +@@ -187,6 +187,10 @@ NSMutableDictionary *RCTNSTextAttributesFromTextAttri + attributes[NSBackgroundColorAttributeName] = RCTEffectiveBackgroundColorFromTextAttributes(textAttributes); + } + ++ if (textAttributes.borderRadius.has_value()) { ++ attributes[RCTTextBorderRadiusAttributeName] = @(*textAttributes.borderRadius); ++ } ++ + // Kerning + if (!isnan(textAttributes.letterSpacing)) { + attributes[NSKernAttributeName] = @(textAttributes.letterSpacing); +diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +index 5fed44e..f4b7d39 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm ++++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +@@ -8,6 +8,7 @@ + #import "RCTTextLayoutManager.h" + + #import "RCTAttributedTextUtils.h" ++#import "RCTTextLayoutManagerWithBorderRadius.h" + + #import + #import +@@ -232,7 +233,7 @@ static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsi + : NSLineBreakByClipping; + textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines; + +- NSLayoutManager *layoutManager = [NSLayoutManager new]; ++ NSLayoutManager *layoutManager = [RCTTextLayoutManagerWithBorderRadius new]; + layoutManager.usesFontLeading = NO; + [layoutManager addTextContainer:textContainer]; + +diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManagerWithBorderRadius.h b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManagerWithBorderRadius.h +new file mode 100644 +index 0000000..3ca52ac +--- /dev/null ++++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManagerWithBorderRadius.h +@@ -0,0 +1,23 @@ ++/* ++ * Copyright (c) Meta Platforms, Inc. and affiliates. ++ * ++ * This source code is licensed under the MIT license found in the ++ * LICENSE file in the root directory of this source tree. ++ */ ++ ++#import ++ ++NS_ASSUME_NONNULL_BEGIN ++ ++/** ++ * Custom NSLayoutManager subclass that draws rounded-rectangle backgrounds ++ * for inline text spans that have the RCTTextBorderRadius attribute set. ++ * ++ * For multiline spans, only the outer corners are rounded: left corners on the ++ * first line rect, right corners on the last line rect (flipped for RTL). ++ */ ++@interface RCTTextLayoutManagerWithBorderRadius : NSLayoutManager ++ ++@end ++ ++NS_ASSUME_NONNULL_END +diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManagerWithBorderRadius.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManagerWithBorderRadius.mm +new file mode 100644 +index 0000000..402cd30 +--- /dev/null ++++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManagerWithBorderRadius.mm +@@ -0,0 +1,75 @@ ++/* ++ * Copyright (c) Meta Platforms, Inc. and affiliates. ++ * ++ * This source code is licensed under the MIT license found in the ++ * LICENSE file in the root directory of this source tree. ++ */ ++ ++#import "RCTTextLayoutManagerWithBorderRadius.h" ++ ++#import "RCTAttributedTextUtils.h" ++ ++@implementation RCTTextLayoutManagerWithBorderRadius ++ ++- (void)fillBackgroundRectArray:(const CGRect *)rectArray ++ count:(NSUInteger)rectCount ++ forCharacterRange:(NSRange)charRange ++ color:(UIColor *)color ++{ ++ NSNumber *borderRadiusValue = [self.textStorage attribute:RCTTextBorderRadiusAttributeName ++ atIndex:charRange.location ++ effectiveRange:nil]; ++ ++ if (borderRadiusValue == nil || borderRadiusValue.floatValue == 0) { ++ [super fillBackgroundRectArray:rectArray count:rectCount forCharacterRange:charRange color:color]; ++ return; ++ } ++ ++ CGFloat borderRadius = borderRadiusValue.floatValue; ++ ++ CGContextRef context = UIGraphicsGetCurrentContext(); ++ if (context == nil) { ++ return; ++ } ++ ++ CGContextSaveGState(context); ++ CGContextSetFillColorWithColor(context, color.CGColor); ++ ++ for (NSUInteger i = 0; i < rectCount; i++) { ++ CGRect rect = rectArray[i]; ++ ++ BOOL isFirstRect = (i == 0); ++ BOOL isLastRect = (i == rectCount - 1); ++ BOOL isSingleRect = (rectCount == 1); ++ ++ UIBezierPath *path; ++ ++ if (isSingleRect) { ++ path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:borderRadius]; ++ } else { ++ UIRectCorner corners = 0; ++ ++ // For multiline: round left corners on first rect, right corners on last rect. ++ // NSLayoutManager provides rects in visual (LTR) order, so this handles ++ // both LTR and RTL correctly - the first rect is always the start of ++ // the span and the last rect is always the end. ++ if (isFirstRect) { ++ corners |= UIRectCornerTopLeft | UIRectCornerBottomLeft; ++ } ++ if (isLastRect) { ++ corners |= UIRectCornerTopRight | UIRectCornerBottomRight; ++ } ++ ++ path = [UIBezierPath bezierPathWithRoundedRect:rect ++ byRoundingCorners:corners ++ cornerRadii:CGSizeMake(borderRadius, borderRadius)]; ++ } ++ ++ CGContextAddPath(context, path.CGPath); ++ CGContextFillPath(context); ++ } ++ ++ CGContextRestoreGState(context); ++} ++ ++@end From 1141023518067b03ac3e24830e36e2e944a59540 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 6 Mar 2026 15:41:11 +0100 Subject: [PATCH 03/14] chore: add patch to details.md --- patches/react-native/details.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/patches/react-native/details.md b/patches/react-native/details.md index 0fa7490749d1..c73764b33321 100644 --- a/patches/react-native/details.md +++ b/patches/react-native/details.md @@ -231,3 +231,20 @@ - Upstream PR/issue: https://github.com/facebook/react-native/pull/55934 - E/App issue: https://github.com/Expensify/App/issues/75120 - PR Introducing Patch: https://github.com/Expensify/App/pull/79962 + +### [react-native+0.83.1+031+nested-text-border-radius.patch](react-native+0.83.1+031+nested-text-border-radius.patch) + +- Reason: + + ``` + Adds borderRadius support for nested backgrounds on iOS and Android. + On the C++ side, a std::optional borderRadius field is added to TextAttributes and wired + through BaseTextProps and conversions. On Android, a custom DrawCommandSpan with + ReactBackgroundDrawSpan draws rounded-rect backgrounds. On iOS, a custom NSLayoutManager subclass + (RCTTextLayoutManagerWithBorderRadius) overrides fillBackgroundRectArray to draw rounded rectangles + using UIBezierPath, with per-line corner rounding for multiline spans. + ``` + +- Upstream PR/issue: 🛑 +- E/App issue: 🛑 +- PR introducing patch: 🛑 From 9f0026b50a75e063e018607cea4e5df83651bacb Mon Sep 17 00:00:00 2001 From: war-in Date: Mon, 23 Mar 2026 12:56:26 +0100 Subject: [PATCH 04/14] chore: rename --- ...ch => react-native+0.83.1+034+nested-text-border-radius.patch} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename patches/react-native/{react-native+0.83.1+031+nested-text-border-radius.patch => react-native+0.83.1+034+nested-text-border-radius.patch} (100%) diff --git a/patches/react-native/react-native+0.83.1+031+nested-text-border-radius.patch b/patches/react-native/react-native+0.83.1+034+nested-text-border-radius.patch similarity index 100% rename from patches/react-native/react-native+0.83.1+031+nested-text-border-radius.patch rename to patches/react-native/react-native+0.83.1+034+nested-text-border-radius.patch From 27f850eee126c119f2e3247e0210cc1d57cf7ba6 Mon Sep 17 00:00:00 2001 From: war-in Date: Mon, 23 Mar 2026 13:15:12 +0100 Subject: [PATCH 05/14] chore: link issue & PR --- patches/react-native/details.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patches/react-native/details.md b/patches/react-native/details.md index 485306164389..d98c4aa2fc86 100644 --- a/patches/react-native/details.md +++ b/patches/react-native/details.md @@ -267,5 +267,5 @@ ``` - Upstream PR/issue: 🛑 -- E/App issue: 🛑 -- PR introducing patch: 🛑 +- E/App issue: https://github.com/Expensify/App/issues/78873 +- PR introducing patch: https://github.com/Expensify/App/pull/84556/changes From ff0fd4ac200e337fa5a25c47680aabcd069063aa Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 25 Mar 2026 16:15:24 +0100 Subject: [PATCH 06/14] fix: remove the isSinleLine branch --- ...0.83.1+034+nested-text-border-radius.patch | 50 +++++++------------ 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/patches/react-native/react-native+0.83.1+034+nested-text-border-radius.patch b/patches/react-native/react-native+0.83.1+034+nested-text-border-radius.patch index 825bd643ee20..1c1a7b5e2478 100644 --- a/patches/react-native/react-native+0.83.1+034+nested-text-border-radius.patch +++ b/patches/react-native/react-native+0.83.1+034+nested-text-border-radius.patch @@ -153,7 +153,7 @@ new file mode 100644 index 0000000..000b569 --- /dev/null +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactBackgroundDrawSpan.kt -@@ -0,0 +1,124 @@ +@@ -0,0 +1,118 @@ +/* Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the @@ -234,14 +234,8 @@ index 0000000..000b569 + } else { + val isFirstLine = line == startLine + val isLastLine = line == endLine -+ val isSingleLine = isFirstLine && isLastLine -+ -+ if (isSingleLine) { -+ canvas.drawRoundRect(rectF, borderRadius, borderRadius, paint) -+ } else { -+ val rtl = layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT -+ drawSelectiveRoundRect(canvas, rectF, isFirstLine, isLastLine, rtl) -+ } ++ val rtl = layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT ++ drawSelectiveRoundRect(canvas, rectF, isFirstLine, isLastLine, rtl) + } + } + } @@ -488,7 +482,7 @@ new file mode 100644 index 0000000..402cd30 --- /dev/null +++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManagerWithBorderRadius.mm -@@ -0,0 +1,75 @@ +@@ -0,0 +1,67 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * @@ -531,30 +525,22 @@ index 0000000..402cd30 + + BOOL isFirstRect = (i == 0); + BOOL isLastRect = (i == rectCount - 1); -+ BOOL isSingleRect = (rectCount == 1); -+ -+ UIBezierPath *path; + -+ if (isSingleRect) { -+ path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:borderRadius]; -+ } else { -+ UIRectCorner corners = 0; -+ -+ // For multiline: round left corners on first rect, right corners on last rect. -+ // NSLayoutManager provides rects in visual (LTR) order, so this handles -+ // both LTR and RTL correctly - the first rect is always the start of -+ // the span and the last rect is always the end. -+ if (isFirstRect) { -+ corners |= UIRectCornerTopLeft | UIRectCornerBottomLeft; -+ } -+ if (isLastRect) { -+ corners |= UIRectCornerTopRight | UIRectCornerBottomRight; -+ } -+ -+ path = [UIBezierPath bezierPathWithRoundedRect:rect -+ byRoundingCorners:corners -+ cornerRadii:CGSizeMake(borderRadius, borderRadius)]; ++ // Round left corners on first rect, right corners on last rect. ++ // NSLayoutManager provides rects in visual (LTR) order, so this handles ++ // both LTR and RTL correctly - the first rect is always the start of ++ // the span and the last rect is always the end. ++ UIRectCorner corners = 0; ++ if (isFirstRect) { ++ corners |= UIRectCornerTopLeft | UIRectCornerBottomLeft; + } ++ if (isLastRect) { ++ corners |= UIRectCornerTopRight | UIRectCornerBottomRight; ++ } ++ ++ UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect ++ byRoundingCorners:corners ++ cornerRadii:CGSizeMake(borderRadius, borderRadius)]; + + CGContextAddPath(context, path.CGPath); + CGContextFillPath(context); From fb457f0c9b446f41a133188b9519ed274ca89d11 Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 2 Apr 2026 16:37:31 +0200 Subject: [PATCH 07/14] fix: rename the patch --- ...ch => react-native+0.83.1+036+nested-text-border-radius.patch} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename patches/react-native/{react-native+0.83.1+034+nested-text-border-radius.patch => react-native+0.83.1+036+nested-text-border-radius.patch} (100%) diff --git a/patches/react-native/react-native+0.83.1+034+nested-text-border-radius.patch b/patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch similarity index 100% rename from patches/react-native/react-native+0.83.1+034+nested-text-border-radius.patch rename to patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch From 924b3c3f0da66764ba9a1bae8646641df1c3ca96 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 12 May 2026 17:43:12 +0200 Subject: [PATCH 08/14] fix: redraw mentions after edit comment --- ...0.83.1+036+nested-text-border-radius.patch | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch b/patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch index 1c1a7b5e2478..77494e03d359 100644 --- a/patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch +++ b/patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch @@ -10,26 +10,29 @@ index 906dfbc..0fb3932 100644 import com.facebook.react.views.text.internal.span.ReactTagSpan; import com.facebook.react.views.text.internal.span.TextInlineImageSpan; import com.facebook.react.views.text.internal.span.TextInlineViewPlaceholderSpan; -@@ -364,6 +365,24 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie +@@ -364,6 +365,27 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie BackgroundStyleApplicator.clipToPaddingBox(this, canvas); } + // Draw custom DrawCommandSpan backgrounds before the text so they appear behind it. -+ // PreparedLayoutTextView handles this natively, but ReactTextView (standard TextView) -+ // does not know about DrawCommandSpan, so we invoke onPreDraw manually here. + Layout layout = getLayout(); + if (spanned != null && layout != null) { -+ DrawCommandSpan[] drawSpans = -+ spanned.getSpans(0, spanned.length(), DrawCommandSpan.class); -+ if (drawSpans != null) { ++ DrawCommandSpan[] drawSpans = spanned.getSpans(0, spanned.length(), DrawCommandSpan.class); ++ if (drawSpans.length > 0) { + int saveCount = canvas.save(); + canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop()); + for (DrawCommandSpan span : drawSpans) { -+ span.onPreDraw( -+ spanned.getSpanStart(span), spanned.getSpanEnd(span), canvas, layout); ++ span.onPreDraw(spanned.getSpanStart(span), spanned.getSpanEnd(span), canvas, layout); + } + canvas.restoreToCount(saveCount); + } ++ } else if (spanned != null) { ++ // getLayout() is null on this draw (typically right after setText() during view ++ // recycling, before the measure pass has rebuilt mLayout). super.onDraw below ++ // will call assumeLayout() and draw the text. Schedule another frame so the next ++ // onDraw — which will find a non-null layout — can paint the DrawCommandSpan ++ // backgrounds underneath. Without this the no-background frame becomes final state. ++ postInvalidateOnAnimation(); + } + super.onDraw(canvas); From 858068c0d554895817d7f7a3308985ee57ad3416 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 19 May 2026 18:01:08 +0200 Subject: [PATCH 09/14] feat: add per-corner borderRadius support --- patches/react-native/details.md | 22 +- ...0.83.1+036+nested-text-border-radius.patch | 406 ++++++++++++++---- ...1+036+rounded-inline-code-background.patch | 155 ------- 3 files changed, 324 insertions(+), 259 deletions(-) delete mode 100644 patches/react-native/react-native+0.83.1+036+rounded-inline-code-background.patch diff --git a/patches/react-native/details.md b/patches/react-native/details.md index 0da48172391f..8a4f56727058 100644 --- a/patches/react-native/details.md +++ b/patches/react-native/details.md @@ -265,24 +265,20 @@ - Upstream PR/issue: [facebook/react-native#51835](https://github.com/facebook/react-native/pull/51835) - E/App issue: [#59953](https://github.com/Expensify/App/issues/59953) -### [react-native+0.83.1+036+rounded-inline-code-background.patch](react-native+0.83.1+036+rounded-inline-code-background.patch) - -- Reason: Draws inline code block background with rounded corners on iOS when `borderTopLeftRadius` is set. -- Upstream PR/issue: 🛑 -- E/App issue: https://github.com/Expensify/App/issues/57556 -- PR introducing patch: https://github.com/Expensify/App/pull/79815 - ### [react-native+0.83.1+036+nested-text-border-radius.patch](react-native+0.83.1+036+nested-text-border-radius.patch) - Reason: ``` - Adds borderRadius support for nested backgrounds on iOS and Android. - On the C++ side, a std::optional borderRadius field is added to TextAttributes and wired - through BaseTextProps and conversions. On Android, a custom DrawCommandSpan with - ReactBackgroundDrawSpan draws rounded-rect backgrounds. On iOS, a custom NSLayoutManager subclass - (RCTTextLayoutManagerWithBorderRadius) overrides fillBackgroundRectArray to draw rounded rectangles - using UIBezierPath, with per-line corner rounding for multiline spans. + Adds borderRadius / per-corner radius support for nested backgrounds on iOS and Android. + On the C++ side, borderRadius + borderTopLeftRadius / borderTopRightRadius / + borderBottomLeftRadius / borderBottomRightRadius fields are added to TextAttributes and wired + through BaseTextProps and conversions. borderRadius acts as a fallback for unset individual + corners; unset corners default to 0 when any radius prop is present. On Android, a custom + DrawCommandSpan with ReactBackgroundDrawSpan draws rounded-rect backgrounds using the four + effective corner radii. On iOS, a custom NSLayoutManager subclass + (RCTTextLayoutManagerWithBorderRadius) overrides fillBackgroundRectArray to draw per-corner + rounded rectangles using CGPath, with per-line outer-corner rounding for multiline spans. ``` - Upstream PR/issue: 🛑 diff --git a/patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch b/patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch index 77494e03d359..7430889b0270 100644 --- a/patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch +++ b/patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java -index 906dfbc..0fb3932 100644 +index 906dfbc..3489b6a 100644 --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -52,6 +52,7 @@ import com.facebook.react.uimanager.style.BorderRadiusProp; @@ -13,7 +13,7 @@ index 906dfbc..0fb3932 100644 @@ -364,6 +365,27 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie BackgroundStyleApplicator.clipToPaddingBox(this, canvas); } - + + // Draw custom DrawCommandSpan backgrounds before the text so they appear behind it. + Layout layout = getLayout(); + if (spanned != null && layout != null) { @@ -39,37 +39,56 @@ index 906dfbc..0fb3932 100644 } } diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt -index 433afa5..b36447c 100644 +index 433afa5..0f1f176 100644 --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt -@@ -68,6 +68,9 @@ public class TextAttributeProps private constructor() { +@@ -68,6 +68,18 @@ public class TextAttributeProps private constructor() { public var isBackgroundColorSet: Boolean = false private set - -+ public var borderRadius: Float = Float.NaN + ++ public var borderTopLeftRadius: Float = Float.NaN ++ private set ++ ++ public var borderTopRightRadius: Float = Float.NaN ++ private set ++ ++ public var borderBottomLeftRadius: Float = Float.NaN ++ private set ++ ++ public var borderBottomRightRadius: Float = Float.NaN + private set + public var opacity: Float = Float.NaN private set - -@@ -376,6 +379,7 @@ public class TextAttributeProps private constructor() { + +@@ -376,6 +388,10 @@ public class TextAttributeProps private constructor() { public const val TA_KEY_ROLE: Int = 26 public const val TA_KEY_TEXT_TRANSFORM: Int = 27 public const val TA_KEY_MAX_FONT_SIZE_MULTIPLIER: Int = 29 -+ public const val TA_KEY_BORDER_RADIUS: Int = 30 - ++ public const val TA_KEY_BORDER_TOP_LEFT_RADIUS: Int = 30 ++ public const val TA_KEY_BORDER_TOP_RIGHT_RADIUS: Int = 31 ++ public const val TA_KEY_BORDER_BOTTOM_LEFT_RADIUS: Int = 32 ++ public const val TA_KEY_BORDER_BOTTOM_RIGHT_RADIUS: Int = 33 + public const val UNSET: Int = -1 - -@@ -430,6 +434,7 @@ public class TextAttributeProps private constructor() { + +@@ -430,6 +446,14 @@ public class TextAttributeProps private constructor() { TA_KEY_TEXT_TRANSFORM -> result.setTextTransform(entry.stringValue) TA_KEY_MAX_FONT_SIZE_MULTIPLIER -> result.maxFontSizeMultiplier = entry.doubleValue.toFloat() -+ TA_KEY_BORDER_RADIUS -> result.borderRadius = entry.doubleValue.toFloat() ++ TA_KEY_BORDER_TOP_LEFT_RADIUS -> ++ result.borderTopLeftRadius = entry.doubleValue.toFloat() ++ TA_KEY_BORDER_TOP_RIGHT_RADIUS -> ++ result.borderTopRightRadius = entry.doubleValue.toFloat() ++ TA_KEY_BORDER_BOTTOM_LEFT_RADIUS -> ++ result.borderBottomLeftRadius = entry.doubleValue.toFloat() ++ TA_KEY_BORDER_BOTTOM_RIGHT_RADIUS -> ++ result.borderBottomRightRadius = entry.doubleValue.toFloat() } } - + diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt -index 5a893fd..a8b8961 100644 +index 5a893fd..5e7895c 100644 --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt @@ -40,6 +40,7 @@ import com.facebook.react.views.text.internal.span.CustomLineHeightSpan @@ -80,14 +99,24 @@ index 5a893fd..a8b8961 100644 import com.facebook.react.views.text.internal.span.ReactClickableSpan import com.facebook.react.views.text.internal.span.ReactForegroundColorSpan import com.facebook.react.views.text.internal.span.ReactFragmentIndexSpan -@@ -269,7 +270,13 @@ internal object TextLayoutManager { +@@ -269,7 +270,23 @@ internal object TextLayoutManager { } if (textAttributes.isBackgroundColorSet) { textAttributes.backgroundColor - ?.let { ReactBackgroundColorSpan(it) } + ?.let { -+ if (!textAttributes.borderRadius.isNaN()) { -+ ReactBackgroundDrawSpan(it, PixelUtil.toPixelFromDIP(textAttributes.borderRadius)) ++ val hasBorderRadius = !textAttributes.borderTopLeftRadius.isNaN() || ++ !textAttributes.borderTopRightRadius.isNaN() || ++ !textAttributes.borderBottomLeftRadius.isNaN() || ++ !textAttributes.borderBottomRightRadius.isNaN() ++ if (hasBorderRadius) { ++ ReactBackgroundDrawSpan( ++ it, ++ PixelUtil.toPixelFromDIP(textAttributes.borderTopLeftRadius.takeIf { r -> !r.isNaN() } ?: 0f), ++ PixelUtil.toPixelFromDIP(textAttributes.borderTopRightRadius.takeIf { r -> !r.isNaN() } ?: 0f), ++ PixelUtil.toPixelFromDIP(textAttributes.borderBottomLeftRadius.takeIf { r -> !r.isNaN() } ?: 0f), ++ PixelUtil.toPixelFromDIP(textAttributes.borderBottomRightRadius.takeIf { r -> !r.isNaN() } ?: 0f), ++ ) + } else { + ReactBackgroundColorSpan(it) + } @@ -95,9 +124,9 @@ index 5a893fd..a8b8961 100644 ?.let { SetSpanOperation(start, end, it) } ?.let { ops.add(it) } } -@@ -438,12 +445,16 @@ internal object TextLayoutManager { +@@ -438,12 +455,25 @@ internal object TextLayoutManager { } - + if (fragment.props.isBackgroundColorSet) { - spannable.setSpan( - fragment.props.backgroundColor?.let { ReactBackgroundColorSpan(it) }, @@ -107,20 +136,29 @@ index 5a893fd..a8b8961 100644 - ) + val bgSpan = + fragment.props.backgroundColor?.let { -+ if (!fragment.props.borderRadius.isNaN()) { ++ val hasBorderRadius = !fragment.props.borderTopLeftRadius.isNaN() || ++ !fragment.props.borderTopRightRadius.isNaN() || ++ !fragment.props.borderBottomLeftRadius.isNaN() || ++ !fragment.props.borderBottomRightRadius.isNaN() ++ if (hasBorderRadius) { + ReactBackgroundDrawSpan( -+ it, PixelUtil.toPixelFromDIP(fragment.props.borderRadius)) ++ it, ++ PixelUtil.toPixelFromDIP(fragment.props.borderTopLeftRadius.takeIf { r -> !r.isNaN() } ?: 0f), ++ PixelUtil.toPixelFromDIP(fragment.props.borderTopRightRadius.takeIf { r -> !r.isNaN() } ?: 0f), ++ PixelUtil.toPixelFromDIP(fragment.props.borderBottomLeftRadius.takeIf { r -> !r.isNaN() } ?: 0f), ++ PixelUtil.toPixelFromDIP(fragment.props.borderBottomRightRadius.takeIf { r -> !r.isNaN() } ?: 0f), ++ ) + } else { + ReactBackgroundColorSpan(it) + } + } + spannable.setSpan(bgSpan, start, end, spanFlags) } - + if (!fragment.props.opacity.isNaN()) { diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/DrawCommandSpan.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/DrawCommandSpan.kt new file mode 100644 -index 0000000..b9d2591 +index 0000000..20d0f7b --- /dev/null +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/DrawCommandSpan.kt @@ -0,0 +1,27 @@ @@ -153,10 +191,10 @@ index 0000000..b9d2591 +} diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactBackgroundDrawSpan.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactBackgroundDrawSpan.kt new file mode 100644 -index 0000000..000b569 +index 0000000..8194ea7 --- /dev/null +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactBackgroundDrawSpan.kt -@@ -0,0 +1,118 @@ +@@ -0,0 +1,123 @@ +/* Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the @@ -181,7 +219,10 @@ index 0000000..000b569 + */ +internal class ReactBackgroundDrawSpan( + @ColorInt private val backgroundColor: Int, -+ private val borderRadius: Float, ++ private val borderTopLeftRadius: Float, ++ private val borderTopRightRadius: Float, ++ private val borderBottomLeftRadius: Float, ++ private val borderBottomRightRadius: Float, +) : DrawCommandSpan() { + + private val paint = @@ -232,7 +273,9 @@ index 0000000..000b569 + + rectF.set(actualLeft, top, actualRight, bottom) + -+ if (borderRadius == 0f) { ++ val hasAnyRadius = borderTopLeftRadius != 0f || borderTopRightRadius != 0f || ++ borderBottomLeftRadius != 0f || borderBottomRightRadius != 0f ++ if (!hasAnyRadius) { + canvas.drawRect(rectF, paint) + } else { + val isFirstLine = line == startLine @@ -250,17 +293,17 @@ index 0000000..000b569 + isLastLine: Boolean, + rtl: Boolean, + ) { -+ // For LTR: first line rounds left corners, last line rounds right corners -+ // For RTL: first line rounds right corners, last line rounds left corners ++ // For LTR: first line rounds left corners, last line rounds right corners. ++ // For RTL: first line rounds right corners, last line rounds left corners. + val roundStart = isFirstLine + val roundEnd = isLastLine + val roundLeft = if (rtl) roundEnd else roundStart + val roundRight = if (rtl) roundStart else roundEnd + -+ val topLeft = if (roundLeft) borderRadius else 0f -+ val bottomLeft = if (roundLeft) borderRadius else 0f -+ val topRight = if (roundRight) borderRadius else 0f -+ val bottomRight = if (roundRight) borderRadius else 0f ++ val topLeft = if (roundLeft) borderTopLeftRadius else 0f ++ val bottomLeft = if (roundLeft) borderBottomLeftRadius else 0f ++ val topRight = if (roundRight) borderTopRightRadius else 0f ++ val bottomRight = if (roundRight) borderBottomRightRadius else 0f + + val radii = + floatArrayOf( @@ -276,97 +319,163 @@ index 0000000..000b569 + } +} diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp -index 9cf89bf..3211ff8 100644 +index 9cf89bf..6eb899f 100644 --- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp +++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp -@@ -27,6 +27,9 @@ void TextAttributes::apply(TextAttributes textAttributes) { +@@ -27,6 +27,21 @@ void TextAttributes::apply(TextAttributes textAttributes) { : backgroundColor; opacity = !std::isnan(textAttributes.opacity) ? textAttributes.opacity : opacity; + borderRadius = textAttributes.borderRadius.has_value() + ? textAttributes.borderRadius + : borderRadius; - ++ borderTopLeftRadius = textAttributes.borderTopLeftRadius.has_value() ++ ? textAttributes.borderTopLeftRadius ++ : borderTopLeftRadius; ++ borderTopRightRadius = textAttributes.borderTopRightRadius.has_value() ++ ? textAttributes.borderTopRightRadius ++ : borderTopRightRadius; ++ borderBottomLeftRadius = textAttributes.borderBottomLeftRadius.has_value() ++ ? textAttributes.borderBottomLeftRadius ++ : borderBottomLeftRadius; ++ borderBottomRightRadius = textAttributes.borderBottomRightRadius.has_value() ++ ? textAttributes.borderBottomRightRadius ++ : borderBottomRightRadius; + // Font fontFamily = !textAttributes.fontFamily.empty() ? textAttributes.fontFamily -@@ -141,7 +144,8 @@ bool TextAttributes::operator==(const TextAttributes& rhs) const { +@@ -141,7 +156,12 @@ bool TextAttributes::operator==(const TextAttributes& rhs) const { layoutDirection, accessibilityRole, role, - textTransform) == + textTransform, -+ borderRadius) == ++ borderRadius, ++ borderTopLeftRadius, ++ borderTopRightRadius, ++ borderBottomLeftRadius, ++ borderBottomRightRadius) == std::tie( rhs.foregroundColor, rhs.backgroundColor, -@@ -164,7 +168,8 @@ bool TextAttributes::operator==(const TextAttributes& rhs) const { +@@ -164,7 +184,12 @@ bool TextAttributes::operator==(const TextAttributes& rhs) const { rhs.layoutDirection, rhs.accessibilityRole, rhs.role, - rhs.textTransform) && + rhs.textTransform, -+ rhs.borderRadius) && ++ rhs.borderRadius, ++ rhs.borderTopLeftRadius, ++ rhs.borderTopRightRadius, ++ rhs.borderBottomLeftRadius, ++ rhs.borderBottomRightRadius) && floatEquality(maxFontSizeMultiplier, rhs.maxFontSizeMultiplier) && floatEquality(opacity, rhs.opacity) && floatEquality(fontSize, rhs.fontSize) && -@@ -199,6 +204,8 @@ SharedDebugStringConvertibleList TextAttributes::getDebugProps() const { +@@ -199,6 +224,24 @@ SharedDebugStringConvertibleList TextAttributes::getDebugProps() const { debugStringConvertibleItem( "foregroundColor", foregroundColor, textAttributes.foregroundColor), debugStringConvertibleItem("opacity", opacity, textAttributes.opacity), + debugStringConvertibleItem( + "borderRadius", borderRadius, textAttributes.borderRadius), - ++ debugStringConvertibleItem( ++ "borderTopLeftRadius", ++ borderTopLeftRadius, ++ textAttributes.borderTopLeftRadius), ++ debugStringConvertibleItem( ++ "borderTopRightRadius", ++ borderTopRightRadius, ++ textAttributes.borderTopRightRadius), ++ debugStringConvertibleItem( ++ "borderBottomLeftRadius", ++ borderBottomLeftRadius, ++ textAttributes.borderBottomLeftRadius), ++ debugStringConvertibleItem( ++ "borderBottomRightRadius", ++ borderBottomRightRadius, ++ textAttributes.borderBottomRightRadius), + // Font debugStringConvertibleItem( diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h -index b664524..ad40ac0 100644 +index b664524..2eaa3bc 100644 --- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h +++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h -@@ -42,6 +42,7 @@ class TextAttributes : public DebugStringConvertible { +@@ -42,6 +42,11 @@ class TextAttributes : public DebugStringConvertible { SharedColor foregroundColor{}; SharedColor backgroundColor{}; Float opacity{std::numeric_limits::quiet_NaN()}; + std::optional borderRadius{}; - ++ std::optional borderTopLeftRadius{}; ++ std::optional borderTopRightRadius{}; ++ std::optional borderBottomLeftRadius{}; ++ std::optional borderBottomRightRadius{}; + // Font std::string fontFamily{""}; -@@ -138,7 +139,8 @@ struct hash { +@@ -138,7 +143,12 @@ struct hash { textAttributes.isPressable, textAttributes.layoutDirection, textAttributes.accessibilityRole, - textAttributes.role); + textAttributes.role, -+ textAttributes.borderRadius); ++ textAttributes.borderRadius, ++ textAttributes.borderTopLeftRadius, ++ textAttributes.borderTopRightRadius, ++ textAttributes.borderBottomLeftRadius, ++ textAttributes.borderBottomRightRadius); } }; } // namespace std diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h -index f771236..a62da2e 100644 +index f771236..48c1fe2 100644 --- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h -@@ -1027,6 +1027,7 @@ constexpr static MapBuffer::Key TA_KEY_ROLE = 26; +@@ -1027,6 +1027,10 @@ constexpr static MapBuffer::Key TA_KEY_ROLE = 26; constexpr static MapBuffer::Key TA_KEY_TEXT_TRANSFORM = 27; constexpr static MapBuffer::Key TA_KEY_ALIGNMENT_VERTICAL = 28; constexpr static MapBuffer::Key TA_KEY_MAX_FONT_SIZE_MULTIPLIER = 29; -+constexpr static MapBuffer::Key TA_KEY_BORDER_RADIUS = 30; - ++constexpr static MapBuffer::Key TA_KEY_BORDER_TOP_LEFT_RADIUS = 30; ++constexpr static MapBuffer::Key TA_KEY_BORDER_TOP_RIGHT_RADIUS = 31; ++constexpr static MapBuffer::Key TA_KEY_BORDER_BOTTOM_LEFT_RADIUS = 32; ++constexpr static MapBuffer::Key TA_KEY_BORDER_BOTTOM_RIGHT_RADIUS = 33; + // constants for ParagraphAttributes serialization constexpr static MapBuffer::Key PA_KEY_MAX_NUMBER_OF_LINES = 0; -@@ -1171,6 +1172,9 @@ inline MapBuffer toMapBuffer(const TextAttributes &textAttributes) +@@ -1171,6 +1175,28 @@ inline MapBuffer toMapBuffer(const TextAttributes &textAttributes) if (textAttributes.role.has_value()) { builder.putInt(TA_KEY_ROLE, static_cast(*textAttributes.role)); } -+ if (textAttributes.borderRadius.has_value()) { -+ builder.putDouble(TA_KEY_BORDER_RADIUS, *textAttributes.borderRadius); ++ { ++ const bool hasAny = textAttributes.borderRadius.has_value() || ++ textAttributes.borderTopLeftRadius.has_value() || ++ textAttributes.borderTopRightRadius.has_value() || ++ textAttributes.borderBottomLeftRadius.has_value() || ++ textAttributes.borderBottomRightRadius.has_value(); ++ if (hasAny) { ++ const Float fallback = textAttributes.borderRadius.value_or(0.0f); ++ builder.putDouble( ++ TA_KEY_BORDER_TOP_LEFT_RADIUS, ++ textAttributes.borderTopLeftRadius.value_or(fallback)); ++ builder.putDouble( ++ TA_KEY_BORDER_TOP_RIGHT_RADIUS, ++ textAttributes.borderTopRightRadius.value_or(fallback)); ++ builder.putDouble( ++ TA_KEY_BORDER_BOTTOM_LEFT_RADIUS, ++ textAttributes.borderBottomLeftRadius.value_or(fallback)); ++ builder.putDouble( ++ TA_KEY_BORDER_BOTTOM_RIGHT_RADIUS, ++ textAttributes.borderBottomRightRadius.value_or(fallback)); ++ } + } return builder.build(); } - + diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp -index f20cd3c..35f21d1 100644 +index f20cd3c..44ef461 100644 --- a/node_modules/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp +++ b/node_modules/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp -@@ -222,6 +222,12 @@ static TextAttributes convertRawProp( +@@ -222,6 +222,36 @@ static TextAttributes convertRawProp( "backgroundColor", sourceTextAttributes.backgroundColor, defaultTextAttributes.backgroundColor); @@ -376,19 +485,51 @@ index f20cd3c..35f21d1 100644 + "borderRadius", + sourceTextAttributes.borderRadius, + defaultTextAttributes.borderRadius); - ++ textAttributes.borderTopLeftRadius = convertRawProp( ++ context, ++ rawProps, ++ "borderTopLeftRadius", ++ sourceTextAttributes.borderTopLeftRadius, ++ defaultTextAttributes.borderTopLeftRadius); ++ textAttributes.borderTopRightRadius = convertRawProp( ++ context, ++ rawProps, ++ "borderTopRightRadius", ++ sourceTextAttributes.borderTopRightRadius, ++ defaultTextAttributes.borderTopRightRadius); ++ textAttributes.borderBottomLeftRadius = convertRawProp( ++ context, ++ rawProps, ++ "borderBottomLeftRadius", ++ sourceTextAttributes.borderBottomLeftRadius, ++ defaultTextAttributes.borderBottomLeftRadius); ++ textAttributes.borderBottomRightRadius = convertRawProp( ++ context, ++ rawProps, ++ "borderBottomRightRadius", ++ sourceTextAttributes.borderBottomRightRadius, ++ defaultTextAttributes.borderBottomRightRadius); + return textAttributes; } -@@ -334,6 +340,8 @@ void BaseTextProps::setProp( +@@ -334,6 +364,16 @@ void BaseTextProps::setProp( defaults, value, textAttributes, opacity, "opacity"); REBUILD_FIELD_SWITCH_CASE( defaults, value, textAttributes, backgroundColor, "backgroundColor"); + REBUILD_FIELD_SWITCH_CASE( + defaults, value, textAttributes, borderRadius, "borderRadius"); ++ REBUILD_FIELD_SWITCH_CASE( ++ defaults, value, textAttributes, borderTopLeftRadius, "borderTopLeftRadius"); ++ REBUILD_FIELD_SWITCH_CASE( ++ defaults, value, textAttributes, borderTopRightRadius, "borderTopRightRadius"); ++ REBUILD_FIELD_SWITCH_CASE( ++ defaults, value, textAttributes, borderBottomLeftRadius, "borderBottomLeftRadius"); ++ REBUILD_FIELD_SWITCH_CASE( ++ defaults, value, textAttributes, borderBottomRightRadius, "borderBottomRightRadius"); } } - -@@ -532,6 +540,12 @@ void BaseTextProps::appendTextAttributesProps( + +@@ -532,6 +572,44 @@ void BaseTextProps::appendTextAttributesProps( oldProps->textAttributes.backgroundColor) { result["backgroundColor"] = *textAttributes.backgroundColor; } @@ -397,12 +538,59 @@ index f20cd3c..35f21d1 100644 + result["borderRadius"] = textAttributes.borderRadius.has_value() + ? textAttributes.borderRadius.value() + : folly::dynamic(nullptr); ++ } ++ ++ if (textAttributes.borderTopLeftRadius != ++ oldProps->textAttributes.borderTopLeftRadius) { ++ result["borderTopLeftRadius"] = ++ textAttributes.borderTopLeftRadius.has_value() ++ ? textAttributes.borderTopLeftRadius.value() ++ : folly::dynamic(nullptr); ++ } ++ ++ if (textAttributes.borderTopRightRadius != ++ oldProps->textAttributes.borderTopRightRadius) { ++ result["borderTopRightRadius"] = ++ textAttributes.borderTopRightRadius.has_value() ++ ? textAttributes.borderTopRightRadius.value() ++ : folly::dynamic(nullptr); ++ } ++ ++ if (textAttributes.borderBottomLeftRadius != ++ oldProps->textAttributes.borderBottomLeftRadius) { ++ result["borderBottomLeftRadius"] = ++ textAttributes.borderBottomLeftRadius.has_value() ++ ? textAttributes.borderBottomLeftRadius.value() ++ : folly::dynamic(nullptr); ++ } ++ ++ if (textAttributes.borderBottomRightRadius != ++ oldProps->textAttributes.borderBottomRightRadius) { ++ result["borderBottomRightRadius"] = ++ textAttributes.borderBottomRightRadius.has_value() ++ ? textAttributes.borderBottomRightRadius.value() ++ : folly::dynamic(nullptr); + } } - + #endif +diff --git a/node_modules/react-native/ReactCommon/react/renderer/consistency/React-rendererconsistency.podspec b/node_modules/react-native/ReactCommon/react/renderer/consistency/React-rendererconsistency.podspec +index 216fd44..95af4c1 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/consistency/React-rendererconsistency.podspec ++++ b/node_modules/react-native/ReactCommon/react/renderer/consistency/React-rendererconsistency.podspec +@@ -16,7 +16,9 @@ else + source[:tag] = "v#{version}" + end + +-header_search_paths = [] ++header_search_paths = [ ++ "\"$(PODS_TARGET_SRCROOT)/ReactCommon/react/renderer/consistency\"", ++] + + if ENV['USE_FRAMEWORKS'] + header_search_paths << "\"$(PODS_TARGET_SRCROOT)/../../..\"" # this is needed to allow the rendererconsistency access its own files diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h -index 902912e..26c04e9 100644 +index dac572b..0c84815 100644 --- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h +++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h @@ -19,6 +19,9 @@ NSString *const RCTAttributedStringEventEmitterKey = @"EventEmitter"; @@ -416,22 +604,35 @@ index 902912e..26c04e9 100644 * Creates `NSTextAttributes` from given `facebook::react::TextAttributes` */ diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm -index f96a049..0e2c71d 100644 +index 520a24c..c456084 100644 --- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm -@@ -187,6 +187,10 @@ NSMutableDictionary *RCTNSTextAttributesFromTextAttri +@@ -182,6 +182,23 @@ NSMutableDictionary *RCTNSTextAttributesFromTextAttri attributes[NSBackgroundColorAttributeName] = RCTEffectiveBackgroundColorFromTextAttributes(textAttributes); } -+ if (textAttributes.borderRadius.has_value()) { -+ attributes[RCTTextBorderRadiusAttributeName] = @(*textAttributes.borderRadius); ++ { ++ const bool hasAny = textAttributes.borderRadius.has_value() || ++ textAttributes.borderTopLeftRadius.has_value() || ++ textAttributes.borderTopRightRadius.has_value() || ++ textAttributes.borderBottomLeftRadius.has_value() || ++ textAttributes.borderBottomRightRadius.has_value(); ++ if (hasAny) { ++ const CGFloat fallback = textAttributes.borderRadius.value_or(0.0f); ++ attributes[RCTTextBorderRadiusAttributeName] = @[ ++ @(textAttributes.borderTopLeftRadius.value_or(fallback)), ++ @(textAttributes.borderTopRightRadius.value_or(fallback)), ++ @(textAttributes.borderBottomLeftRadius.value_or(fallback)), ++ @(textAttributes.borderBottomRightRadius.value_or(fallback)), ++ ]; ++ } + } + // Kerning if (!isnan(textAttributes.letterSpacing)) { attributes[NSKernAttributeName] = @(textAttributes.letterSpacing); diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -index 5fed44e..f4b7d39 100644 +index ef50487..d82b6a7 100644 --- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm @@ -8,6 +8,7 @@ @@ -482,10 +683,10 @@ index 0000000..3ca52ac +NS_ASSUME_NONNULL_END diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManagerWithBorderRadius.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManagerWithBorderRadius.mm new file mode 100644 -index 0000000..402cd30 +index 0000000..e6ec24a --- /dev/null +++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManagerWithBorderRadius.mm -@@ -0,0 +1,67 @@ +@@ -0,0 +1,90 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * @@ -504,16 +705,26 @@ index 0000000..402cd30 + forCharacterRange:(NSRange)charRange + color:(UIColor *)color +{ -+ NSNumber *borderRadiusValue = [self.textStorage attribute:RCTTextBorderRadiusAttributeName -+ atIndex:charRange.location -+ effectiveRange:nil]; ++ NSArray *radii = [self.textStorage attribute:RCTTextBorderRadiusAttributeName ++ atIndex:charRange.location ++ effectiveRange:nil]; + -+ if (borderRadiusValue == nil || borderRadiusValue.floatValue == 0) { ++ if (radii == nil) { + [super fillBackgroundRectArray:rectArray count:rectCount forCharacterRange:charRange color:color]; + return; + } + -+ CGFloat borderRadius = borderRadiusValue.floatValue; ++ // radii is @[topLeft, topRight, bottomLeft, bottomRight] ++ CGFloat tl = radii[0].floatValue; ++ CGFloat tr = radii[1].floatValue; ++ CGFloat bl = radii[2].floatValue; ++ CGFloat br = radii[3].floatValue; ++ ++ BOOL hasAnyRadius = tl > 0 || tr > 0 || bl > 0 || br > 0; ++ if (!hasAnyRadius) { ++ [super fillBackgroundRectArray:rectArray count:rectCount forCharacterRange:charRange color:color]; ++ return; ++ } + + CGContextRef context = UIGraphicsGetCurrentContext(); + if (context == nil) { @@ -529,24 +740,37 @@ index 0000000..402cd30 + BOOL isFirstRect = (i == 0); + BOOL isLastRect = (i == rectCount - 1); + -+ // Round left corners on first rect, right corners on last rect. -+ // NSLayoutManager provides rects in visual (LTR) order, so this handles -+ // both LTR and RTL correctly - the first rect is always the start of -+ // the span and the last rect is always the end. -+ UIRectCorner corners = 0; -+ if (isFirstRect) { -+ corners |= UIRectCornerTopLeft | UIRectCornerBottomLeft; -+ } -+ if (isLastRect) { -+ corners |= UIRectCornerTopRight | UIRectCornerBottomRight; -+ } ++ // Round left corners on the first rect, right corners on the last rect. ++ // NSLayoutManager provides rects in visual order so this handles LTR and RTL correctly. ++ CGFloat effectiveTL = isFirstRect ? tl : 0; ++ CGFloat effectiveBL = isFirstRect ? bl : 0; ++ CGFloat effectiveTR = isLastRect ? tr : 0; ++ CGFloat effectiveBR = isLastRect ? br : 0; + + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect -+ byRoundingCorners:corners -+ cornerRadii:CGSizeMake(borderRadius, borderRadius)]; -+ -+ CGContextAddPath(context, path.CGPath); ++ byRoundingCorners:UIRectCornerAllCorners ++ cornerRadii:CGSizeMake(1, 1)]; ++ // bezierPathWithRoundedRect:byRoundingCorners: only supports uniform radii per corner mask. ++ // Use a CGPath with per-corner radii instead. ++ CGMutablePathRef cgPath = CGPathCreateMutable(); ++ CGFloat x = CGRectGetMinX(rect); ++ CGFloat y = CGRectGetMinY(rect); ++ CGFloat w = CGRectGetWidth(rect); ++ CGFloat h = CGRectGetHeight(rect); ++ CGPathMoveToPoint(cgPath, NULL, x + effectiveTL, y); ++ CGPathAddLineToPoint(cgPath, NULL, x + w - effectiveTR, y); ++ CGPathAddArcToPoint(cgPath, NULL, x + w, y, x + w, y + effectiveTR, effectiveTR); ++ CGPathAddLineToPoint(cgPath, NULL, x + w, y + h - effectiveBR); ++ CGPathAddArcToPoint(cgPath, NULL, x + w, y + h, x + w - effectiveBR, y + h, effectiveBR); ++ CGPathAddLineToPoint(cgPath, NULL, x + effectiveBL, y + h); ++ CGPathAddArcToPoint(cgPath, NULL, x, y + h, x, y + h - effectiveBL, effectiveBL); ++ CGPathAddLineToPoint(cgPath, NULL, x, y + effectiveTL); ++ CGPathAddArcToPoint(cgPath, NULL, x, y, x + effectiveTL, y, effectiveTL); ++ CGPathCloseSubpath(cgPath); ++ ++ CGContextAddPath(context, cgPath); + CGContextFillPath(context); ++ CGPathRelease(cgPath); + } + + CGContextRestoreGState(context); diff --git a/patches/react-native/react-native+0.83.1+036+rounded-inline-code-background.patch b/patches/react-native/react-native+0.83.1+036+rounded-inline-code-background.patch deleted file mode 100644 index 2f8403c6596c..000000000000 --- a/patches/react-native/react-native+0.83.1+036+rounded-inline-code-background.patch +++ /dev/null @@ -1,155 +0,0 @@ -diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp -index 9cf89bf..845f6c1 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp -+++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp -@@ -25,6 +25,9 @@ void TextAttributes::apply(TextAttributes textAttributes) { - backgroundColor = textAttributes.backgroundColor - ? textAttributes.backgroundColor - : backgroundColor; -+ borderTopLeftRadius = !std::isnan(textAttributes.borderTopLeftRadius) -+ ? textAttributes.borderTopLeftRadius -+ : borderTopLeftRadius; - opacity = - !std::isnan(textAttributes.opacity) ? textAttributes.opacity : opacity; - -@@ -171,6 +174,7 @@ bool TextAttributes::operator==(const TextAttributes& rhs) const { - floatEquality(fontSizeMultiplier, rhs.fontSizeMultiplier) && - floatEquality(letterSpacing, rhs.letterSpacing) && - floatEquality(lineHeight, rhs.lineHeight) && -+ floatEquality(borderTopLeftRadius, rhs.borderTopLeftRadius) && - floatEquality(textShadowRadius, rhs.textShadowRadius); - } - -diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h -index b664524..dce4e3b 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h -+++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h -@@ -41,6 +41,7 @@ class TextAttributes : public DebugStringConvertible { - // Color - SharedColor foregroundColor{}; - SharedColor backgroundColor{}; -+ Float borderTopLeftRadius{std::numeric_limits::quiet_NaN()}; - Float opacity{std::numeric_limits::quiet_NaN()}; - - // Font -@@ -112,6 +113,7 @@ struct hash { - return facebook::react::hash_combine( - textAttributes.foregroundColor, - textAttributes.backgroundColor, -+ textAttributes.borderTopLeftRadius, - textAttributes.opacity, - textAttributes.fontFamily, - textAttributes.fontSize, -diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp -index f20cd3c..8a9cedc 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp -+++ b/node_modules/react-native/ReactCommon/react/renderer/components/text/BaseTextProps.cpp -@@ -222,6 +222,12 @@ static TextAttributes convertRawProp( - "backgroundColor", - sourceTextAttributes.backgroundColor, - defaultTextAttributes.backgroundColor); -+ textAttributes.borderTopLeftRadius = convertRawProp( -+ context, -+ rawProps, -+ "borderTopLeftRadius", -+ sourceTextAttributes.borderTopLeftRadius, -+ defaultTextAttributes.borderTopLeftRadius); - - return textAttributes; - } -@@ -334,6 +340,8 @@ void BaseTextProps::setProp( - defaults, value, textAttributes, opacity, "opacity"); - REBUILD_FIELD_SWITCH_CASE( - defaults, value, textAttributes, backgroundColor, "backgroundColor"); -+ REBUILD_FIELD_SWITCH_CASE( -+ defaults, value, textAttributes, borderTopLeftRadius, "borderTopLeftRadius"); - } - } - -diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h -index dac572b..87cc9f0 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h -+++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h -@@ -18,6 +18,7 @@ NSString *const RCTAttributedStringEventEmitterKey = @"EventEmitter"; - - // String representation of either `role` or `accessibilityRole` - NSString *const RCTTextAttributesAccessibilityRoleAttributeName = @"AccessibilityRole"; -+NSString *const RCTTextBackgroundBorderRadiusAttributeName = @"TextBackgroundBorderRadius"; - - /* - * Creates `NSTextAttributes` from given `facebook::react::TextAttributes` -diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm -index 520a24c..452a0fe 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm -+++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm -@@ -179,7 +179,12 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex - } - - if (textAttributes.backgroundColor || !isnan(textAttributes.opacity)) { -- attributes[NSBackgroundColorAttributeName] = RCTEffectiveBackgroundColorFromTextAttributes(textAttributes); -+ UIColor *bgColor = RCTEffectiveBackgroundColorFromTextAttributes(textAttributes) ?: [UIColor clearColor]; -+ if (!isnan(textAttributes.borderTopLeftRadius) && textAttributes.borderTopLeftRadius > 0) { -+ attributes[RCTTextBackgroundBorderRadiusAttributeName] = @[@(textAttributes.borderTopLeftRadius), bgColor]; -+ } else { -+ attributes[NSBackgroundColorAttributeName] = bgColor; -+ } - } - - // Kerning -diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -index ef50487..df9290f 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -+++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -@@ -17,6 +17,8 @@ - - using namespace facebook::react; - -+static const CGFloat inlineCodeBackgroundTopInset = 3; -+ - @implementation RCTTextLayoutManager { - SimpleThreadSafeCache, 256> _cache; - } -@@ -86,6 +88,43 @@ - (void)drawAttributedString:(AttributedString)attributedString - - NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; - -+ NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL]; -+ [textStorage enumerateAttribute:RCTTextBackgroundBorderRadiusAttributeName -+ inRange:characterRange -+ options:0 -+ usingBlock:^(NSArray *value, NSRange range, BOOL *stop) { -+ if (!value) { -+ return; -+ } -+ CGFloat radius = [value[0] floatValue]; -+ UIColor *color = value[1]; -+ NSRange rangeGlyphRange = [layoutManager glyphRangeForCharacterRange:range actualCharacterRange:NULL]; -+ NSMutableArray *rects = [NSMutableArray array]; -+ [layoutManager enumerateEnclosingRectsForGlyphRange:rangeGlyphRange -+ withinSelectedGlyphRange:rangeGlyphRange -+ inTextContainer:textContainer -+ usingBlock:^(CGRect enclosingRect, BOOL *anotherStop) { -+ [rects addObject:[NSValue valueWithCGRect:enclosingRect]]; -+ }]; -+ NSUInteger count = rects.count; -+ for (NSUInteger i = 0; i < count; i++) { -+ CGRect rect = UIEdgeInsetsInsetRect( -+ CGRectOffset([rects[i] CGRectValue], frame.origin.x, frame.origin.y), -+ UIEdgeInsetsMake(inlineCodeBackgroundTopInset, 0, 0, 0)); -+ UIRectCorner corners = 0; -+ if (count == 1) { -+ corners = UIRectCornerAllCorners; -+ } else if (i == 0) { -+ corners = UIRectCornerTopLeft | UIRectCornerBottomLeft; -+ } else if (i == count - 1) { -+ corners = UIRectCornerTopRight | UIRectCornerBottomRight; -+ } -+ UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:CGSizeMake(radius, radius)]; -+ [color setFill]; -+ [path fill]; -+ } -+ }]; -+ - [self processTruncatedAttributedText:textStorage textContainer:textContainer layoutManager:layoutManager]; - - [layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:frame.origin]; From 7b249a799820c14637d60f8b84d3523ab5d60951 Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 20 May 2026 16:01:27 +0200 Subject: [PATCH 10/14] fix: remove unnecessary entries from the patch --- ...0.83.1+036+nested-text-border-radius.patch | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch b/patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch index 7430889b0270..4d9a2ea19fd5 100644 --- a/patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch +++ b/patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch @@ -574,21 +574,6 @@ index f20cd3c..44ef461 100644 } #endif -diff --git a/node_modules/react-native/ReactCommon/react/renderer/consistency/React-rendererconsistency.podspec b/node_modules/react-native/ReactCommon/react/renderer/consistency/React-rendererconsistency.podspec -index 216fd44..95af4c1 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/consistency/React-rendererconsistency.podspec -+++ b/node_modules/react-native/ReactCommon/react/renderer/consistency/React-rendererconsistency.podspec -@@ -16,7 +16,9 @@ else - source[:tag] = "v#{version}" - end - --header_search_paths = [] -+header_search_paths = [ -+ "\"$(PODS_TARGET_SRCROOT)/ReactCommon/react/renderer/consistency\"", -+] - - if ENV['USE_FRAMEWORKS'] - header_search_paths << "\"$(PODS_TARGET_SRCROOT)/../../..\"" # this is needed to allow the rendererconsistency access its own files diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h index dac572b..0c84815 100644 --- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h @@ -686,7 +671,7 @@ new file mode 100644 index 0000000..e6ec24a --- /dev/null +++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManagerWithBorderRadius.mm -@@ -0,0 +1,90 @@ +@@ -0,0 +1,85 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * @@ -747,11 +732,6 @@ index 0000000..e6ec24a + CGFloat effectiveTR = isLastRect ? tr : 0; + CGFloat effectiveBR = isLastRect ? br : 0; + -+ UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect -+ byRoundingCorners:UIRectCornerAllCorners -+ cornerRadii:CGSizeMake(1, 1)]; -+ // bezierPathWithRoundedRect:byRoundingCorners: only supports uniform radii per corner mask. -+ // Use a CGPath with per-corner radii instead. + CGMutablePathRef cgPath = CGPathCreateMutable(); + CGFloat x = CGRectGetMinX(rect); + CGFloat y = CGRectGetMinY(rect); From 73b4a942b7a5affc1aa8cc3de835023415eb5e59 Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 21 May 2026 11:11:15 +0200 Subject: [PATCH 11/14] chore: rename patch --- ...ch => react-native+0.83.1+037+nested-text-border-radius.patch} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename patches/react-native/{react-native+0.83.1+036+nested-text-border-radius.patch => react-native+0.83.1+037+nested-text-border-radius.patch} (100%) diff --git a/patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch b/patches/react-native/react-native+0.83.1+037+nested-text-border-radius.patch similarity index 100% rename from patches/react-native/react-native+0.83.1+036+nested-text-border-radius.patch rename to patches/react-native/react-native+0.83.1+037+nested-text-border-radius.patch From eb6e6f498398bf0607bd07c23481de5a4dd4e539 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 22 May 2026 12:38:44 +0200 Subject: [PATCH 12/14] chore: update details.md --- patches/react-native/details.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/patches/react-native/details.md b/patches/react-native/details.md index 716ffac23fe5..eb53143bf9bf 100644 --- a/patches/react-native/details.md +++ b/patches/react-native/details.md @@ -271,13 +271,6 @@ - Upstream PR/issue: [facebook/react-native#55398](https://github.com/facebook/react-native/pull/55398) - E/App issue: [#90623](https://github.com/Expensify/App/issues/90623) -### [react-native+0.83.1+036+rounded-inline-code-background.patch](react-native+0.83.1+036+rounded-inline-code-background.patch) - -- Reason: Draws inline code block background with rounded corners on iOS when `borderTopLeftRadius` is set. -- Upstream PR/issue: 🛑 -- E/App issue: https://github.com/Expensify/App/issues/57556 -- PR introducing patch: https://github.com/Expensify/App/pull/79815 - ### [react-native+0.83.1+037+nested-text-border-radius.patch](react-native+0.83.1+037+nested-text-border-radius.patch) - Reason: From 7fff63b8e4403a79aedb99f844a0d50461cfff07 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 29 May 2026 16:45:03 +0200 Subject: [PATCH 13/14] fix: draw only background behind the text --- ...0.83.1+038+nested-text-border-radius.patch | 208 +++++++++++------- 1 file changed, 125 insertions(+), 83 deletions(-) diff --git a/patches/react-native/react-native+0.83.1+038+nested-text-border-radius.patch b/patches/react-native/react-native+0.83.1+038+nested-text-border-radius.patch index 4d9a2ea19fd5..59ead44061b7 100644 --- a/patches/react-native/react-native+0.83.1+038+nested-text-border-radius.patch +++ b/patches/react-native/react-native+0.83.1+038+nested-text-border-radius.patch @@ -194,7 +194,7 @@ new file mode 100644 index 0000000..8194ea7 --- /dev/null +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactBackgroundDrawSpan.kt -@@ -0,0 +1,123 @@ +@@ -0,0 +1,125 @@ +/* Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the @@ -265,8 +265,10 @@ index 0000000..8194ea7 + layout.getPrimaryHorizontal(end) + } + -+ val top = layout.getLineTop(line).toFloat() -+ val bottom = layout.getLineBottom(line).toFloat() ++ val baseline = layout.getLineBaseline(line).toFloat() ++ val fm = layout.paint.fontMetrics ++ val top = baseline + fm.ascent ++ val bottom = baseline + fm.descent + + val actualLeft = minOf(left, right) + val actualRight = maxOf(left, right) @@ -589,33 +591,42 @@ index dac572b..0c84815 100644 * Creates `NSTextAttributes` from given `facebook::react::TextAttributes` */ diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm -index 520a24c..c456084 100644 +index 520a24c..3bec610 100644 --- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm -@@ -182,6 +182,23 @@ NSMutableDictionary *RCTNSTextAttributesFromTextAttri - attributes[NSBackgroundColorAttributeName] = RCTEffectiveBackgroundColorFromTextAttributes(textAttributes); +@@ -178,8 +178,30 @@ NSMutableDictionary *RCTNSTextAttributesFromTextAttri + attributes[NSForegroundColorAttributeName] = effectiveForegroundColor; } +- if (textAttributes.backgroundColor || !isnan(textAttributes.opacity)) { +- attributes[NSBackgroundColorAttributeName] = RCTEffectiveBackgroundColorFromTextAttributes(textAttributes); + { -+ const bool hasAny = textAttributes.borderRadius.has_value() || ++ const bool hasAnyRadius = textAttributes.borderRadius.has_value() || + textAttributes.borderTopLeftRadius.has_value() || + textAttributes.borderTopRightRadius.has_value() || + textAttributes.borderBottomLeftRadius.has_value() || + textAttributes.borderBottomRightRadius.has_value(); -+ if (hasAny) { -+ const CGFloat fallback = textAttributes.borderRadius.value_or(0.0f); -+ attributes[RCTTextBorderRadiusAttributeName] = @[ -+ @(textAttributes.borderTopLeftRadius.value_or(fallback)), -+ @(textAttributes.borderTopRightRadius.value_or(fallback)), -+ @(textAttributes.borderBottomLeftRadius.value_or(fallback)), -+ @(textAttributes.borderBottomRightRadius.value_or(fallback)), -+ ]; -+ } -+ } + ++ if (textAttributes.backgroundColor || !isnan(textAttributes.opacity)) { ++ UIColor *bgColor = RCTEffectiveBackgroundColorFromTextAttributes(textAttributes); ++ if (hasAnyRadius) { ++ // Store color alongside radii and skip NSBackgroundColorAttributeName so ++ // NSLayoutManager doesn't draw a flat rect before our per-line rounded one. ++ const CGFloat fallback = textAttributes.borderRadius.value_or(0.0f); ++ attributes[RCTTextBorderRadiusAttributeName] = @[ ++ bgColor, ++ @(textAttributes.borderTopLeftRadius.value_or(fallback)), ++ @(textAttributes.borderTopRightRadius.value_or(fallback)), ++ @(textAttributes.borderBottomLeftRadius.value_or(fallback)), ++ @(textAttributes.borderBottomRightRadius.value_or(fallback)), ++ ]; ++ } else { ++ attributes[NSBackgroundColorAttributeName] = bgColor; ++ } ++ } + } + // Kerning - if (!isnan(textAttributes.letterSpacing)) { - attributes[NSKernAttributeName] = @(textAttributes.letterSpacing); diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm index ef50487..d82b6a7 100644 --- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm @@ -668,10 +679,10 @@ index 0000000..3ca52ac +NS_ASSUME_NONNULL_END diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManagerWithBorderRadius.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManagerWithBorderRadius.mm new file mode 100644 -index 0000000..e6ec24a +index 0000000..fd401a9 --- /dev/null +++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManagerWithBorderRadius.mm -@@ -0,0 +1,85 @@ +@@ -0,0 +1,116 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * @@ -685,75 +696,106 @@ index 0000000..e6ec24a + +@implementation RCTTextLayoutManagerWithBorderRadius + -+- (void)fillBackgroundRectArray:(const CGRect *)rectArray -+ count:(NSUInteger)rectCount -+ forCharacterRange:(NSRange)charRange -+ color:(UIColor *)color ++- (void)drawBackgroundForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin +{ -+ NSArray *radii = [self.textStorage attribute:RCTTextBorderRadiusAttributeName -+ atIndex:charRange.location -+ effectiveRange:nil]; ++ // Let super handle all spans that use NSBackgroundColorAttributeName (no border radius). ++ [super drawBackgroundForGlyphRange:glyphsToShow atPoint:origin]; + -+ if (radii == nil) { -+ [super fillBackgroundRectArray:rectArray count:rectCount forCharacterRange:charRange color:color]; -+ return; -+ } ++ NSRange charRange = [self characterRangeForGlyphRange:glyphsToShow actualGlyphRange:nil]; + -+ // radii is @[topLeft, topRight, bottomLeft, bottomRight] -+ CGFloat tl = radii[0].floatValue; -+ CGFloat tr = radii[1].floatValue; -+ CGFloat bl = radii[2].floatValue; -+ CGFloat br = radii[3].floatValue; ++ [self.textStorage enumerateAttribute:RCTTextBorderRadiusAttributeName ++ inRange:charRange ++ options:0 ++ usingBlock:^(id value, NSRange attrRange, BOOL *stop) { ++ if (!value) { ++ return; ++ } + -+ BOOL hasAnyRadius = tl > 0 || tr > 0 || bl > 0 || br > 0; -+ if (!hasAnyRadius) { -+ [super fillBackgroundRectArray:rectArray count:rectCount forCharacterRange:charRange color:color]; -+ return; -+ } ++ // attrs is @[UIColor, tlRadius, trRadius, blRadius, brRadius] ++ NSArray *attrs = (NSArray *)value; ++ UIColor *color = attrs[0]; ++ CGFloat tl = [attrs[1] floatValue]; ++ CGFloat tr = [attrs[2] floatValue]; ++ CGFloat bl = [attrs[3] floatValue]; ++ CGFloat br = [attrs[4] floatValue]; ++ ++ NSRange spanGlyphRange = [self glyphRangeForCharacterRange:attrRange actualCharacterRange:nil]; ++ ++ __block NSUInteger totalLines = 0; ++ [self enumerateLineFragmentsForGlyphRange:spanGlyphRange ++ usingBlock:^(CGRect r, CGRect u, NSTextContainer *tc, NSRange gr, BOOL *s) { ++ totalLines++; ++ }]; ++ ++ UIFont *font = [self.textStorage attribute:NSFontAttributeName ++ atIndex:attrRange.location ++ effectiveRange:nil]; ++ ++ CGContextRef context = UIGraphicsGetCurrentContext(); ++ if (!context) { ++ return; ++ } + -+ CGContextRef context = UIGraphicsGetCurrentContext(); -+ if (context == nil) { -+ return; -+ } ++ CGContextSaveGState(context); ++ CGContextSetFillColorWithColor(context, color.CGColor); ++ ++ __block NSUInteger lineIdx = 0; ++ [self enumerateLineFragmentsForGlyphRange:spanGlyphRange ++ usingBlock:^(CGRect lineRect, CGRect usedRect, NSTextContainer *tc, NSRange lineGlyphRange, BOOL *s) { ++ NSRange intersected = NSIntersectionRange(spanGlyphRange, lineGlyphRange); ++ CGRect spanRect = [self boundingRectForGlyphRange:intersected inTextContainer:tc]; ++ ++ // Container coords → view coords ++ spanRect.origin.x += origin.x; ++ spanRect.origin.y += origin.y; ++ ++ // Shrink height to em-box (font.lineHeight = ascender + |descender|, no leading). ++ // This creates a leading-sized gap between adjacent line backgrounds, matching web CSS. ++ if (font && intersected.length > 0) { ++ CGPoint glyphLoc = [self locationForGlyphAtIndex:intersected.location]; ++ CGFloat baseline = lineRect.origin.y + origin.y + glyphLoc.y; ++ spanRect.origin.y = baseline - font.ascender; ++ spanRect.size.height = font.lineHeight; ++ } + -+ CGContextSaveGState(context); -+ CGContextSetFillColorWithColor(context, color.CGColor); -+ -+ for (NSUInteger i = 0; i < rectCount; i++) { -+ CGRect rect = rectArray[i]; -+ -+ BOOL isFirstRect = (i == 0); -+ BOOL isLastRect = (i == rectCount - 1); -+ -+ // Round left corners on the first rect, right corners on the last rect. -+ // NSLayoutManager provides rects in visual order so this handles LTR and RTL correctly. -+ CGFloat effectiveTL = isFirstRect ? tl : 0; -+ CGFloat effectiveBL = isFirstRect ? bl : 0; -+ CGFloat effectiveTR = isLastRect ? tr : 0; -+ CGFloat effectiveBR = isLastRect ? br : 0; -+ -+ CGMutablePathRef cgPath = CGPathCreateMutable(); -+ CGFloat x = CGRectGetMinX(rect); -+ CGFloat y = CGRectGetMinY(rect); -+ CGFloat w = CGRectGetWidth(rect); -+ CGFloat h = CGRectGetHeight(rect); -+ CGPathMoveToPoint(cgPath, NULL, x + effectiveTL, y); -+ CGPathAddLineToPoint(cgPath, NULL, x + w - effectiveTR, y); -+ CGPathAddArcToPoint(cgPath, NULL, x + w, y, x + w, y + effectiveTR, effectiveTR); -+ CGPathAddLineToPoint(cgPath, NULL, x + w, y + h - effectiveBR); -+ CGPathAddArcToPoint(cgPath, NULL, x + w, y + h, x + w - effectiveBR, y + h, effectiveBR); -+ CGPathAddLineToPoint(cgPath, NULL, x + effectiveBL, y + h); -+ CGPathAddArcToPoint(cgPath, NULL, x, y + h, x, y + h - effectiveBL, effectiveBL); -+ CGPathAddLineToPoint(cgPath, NULL, x, y + effectiveTL); -+ CGPathAddArcToPoint(cgPath, NULL, x, y, x + effectiveTL, y, effectiveTL); -+ CGPathCloseSubpath(cgPath); -+ -+ CGContextAddPath(context, cgPath); -+ CGContextFillPath(context); -+ CGPathRelease(cgPath); -+ } ++ BOOL isFirst = (lineIdx == 0); ++ BOOL isLast = (lineIdx == totalLines - 1); ++ ++ CGFloat effectiveTL = isFirst ? tl : 0; ++ CGFloat effectiveBL = isFirst ? bl : 0; ++ CGFloat effectiveTR = isLast ? tr : 0; ++ CGFloat effectiveBR = isLast ? br : 0; ++ ++ CGFloat x = CGRectGetMinX(spanRect); ++ CGFloat y = CGRectGetMinY(spanRect); ++ CGFloat w = CGRectGetWidth(spanRect); ++ CGFloat h = CGRectGetHeight(spanRect); ++ ++ BOOL hasAnyRadius = tl > 0 || tr > 0 || bl > 0 || br > 0; ++ if (!hasAnyRadius) { ++ CGContextFillRect(context, spanRect); ++ } else { ++ CGMutablePathRef path = CGPathCreateMutable(); ++ CGPathMoveToPoint(path, NULL, x + effectiveTL, y); ++ CGPathAddLineToPoint(path, NULL, x + w - effectiveTR, y); ++ CGPathAddArcToPoint(path, NULL, x + w, y, x + w, y + effectiveTR, effectiveTR); ++ CGPathAddLineToPoint(path, NULL, x + w, y + h - effectiveBR); ++ CGPathAddArcToPoint(path, NULL, x + w, y + h, x + w - effectiveBR, y + h, effectiveBR); ++ CGPathAddLineToPoint(path, NULL, x + effectiveBL, y + h); ++ CGPathAddArcToPoint(path, NULL, x, y + h, x, y + h - effectiveBL, effectiveBL); ++ CGPathAddLineToPoint(path, NULL, x, y + effectiveTL); ++ CGPathAddArcToPoint(path, NULL, x, y, x + effectiveTL, y, effectiveTL); ++ CGPathCloseSubpath(path); ++ CGContextAddPath(context, path); ++ CGContextFillPath(context); ++ CGPathRelease(path); ++ } ++ ++ lineIdx++; ++ }]; + -+ CGContextRestoreGState(context); ++ CGContextRestoreGState(context); ++ }]; +} + +@end From c5308a2f57525b1ac1edc4c28d050fd9bb1a75dc Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Jun 2026 16:38:27 +0200 Subject: [PATCH 14/14] chore: rename patch --- patches/react-native/details.md | 2 +- ... => react-native+0.83.1+039+nested-text-border-radius.patch} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename patches/react-native/{react-native+0.83.1+038+nested-text-border-radius.patch => react-native+0.83.1+039+nested-text-border-radius.patch} (100%) diff --git a/patches/react-native/details.md b/patches/react-native/details.md index 24a600fd0341..7a173d226434 100644 --- a/patches/react-native/details.md +++ b/patches/react-native/details.md @@ -286,7 +286,7 @@ - E/App issue: https://github.com/Expensify/App/issues/92413 - PR introducing patch: https://github.com/Expensify/App/pull/92918 -### [react-native+0.83.1+038+nested-text-border-radius.patch](react-native+0.83.1+038+nested-text-border-radius.patch) +### [react-native+0.83.1+039+nested-text-border-radius.patch](react-native+0.83.1+039+nested-text-border-radius.patch) - Reason: diff --git a/patches/react-native/react-native+0.83.1+038+nested-text-border-radius.patch b/patches/react-native/react-native+0.83.1+039+nested-text-border-radius.patch similarity index 100% rename from patches/react-native/react-native+0.83.1+038+nested-text-border-radius.patch rename to patches/react-native/react-native+0.83.1+039+nested-text-border-radius.patch