From 9d4f0aff54712127fb4a52627f6b4e1b947a3042 Mon Sep 17 00:00:00 2001 From: Evan Jacobs Date: Mon, 11 May 2026 10:13:13 -0400 Subject: [PATCH 1/2] feat(android): honor textDecorationStyle on Text decorations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `textDecorationStyle` is declared on `TextStyleAndroid` in the public types but `TA_KEY_TEXT_DECORATION_STYLE` was a no-op handler: every value silently rendered as a solid line. This PR wires the prop through the existing C++ → Kotlin pipeline and implements `solid`, `double`, `dotted`, `dashed`, and `wavy` for both underlines and strikethroughs. Background: Android's `Layout.draw` paints the underline produced by `setUnderlineText(true)` using `paint.color`, ignoring `paint.underlineColor` on every API level, and offers no native way to draw a dotted / dashed / wavy decoration. The same applies to strikethrough. `ReactUnderlineSpan` and `ReactStrikethroughSpan` now extend `DrawCommandSpan` and paint the decoration themselves in `onDraw` via `Canvas.drawLine` / `Canvas.drawPath`, dispatching by style. This also makes `textDecorationColor` reach the paint as a side effect, closing a separate long-standing gap (see #4579 from 2015). `TextDecorationStyle::Wavy` is added to the Fabric C++ primitives / conversions so the JS value flows through instead of being rejected with an `Unsupported value` log; the same enum is shared with iOS. The wavy curve uses Chromium/Blink's formula from `decoration_line_painter.cc` (`wavelength = 1 + 2 * round(2 * thickness + 0.5)`, `controlPointDistance = 0.5 + round(3 * thickness + 0.5)`, one cubic Bezier per wavelength with both control points at the midpoint, one above and one below the y-axis). The minimum stroke thickness is density-aware (1.5 dp) so decorations read consistently across display densities. The drawing loop iterates `while x < x2` so the final cycle continues through the last character (including trailing punctuation that would otherwise be visually uncovered when the run width is not an integer multiple of the wavelength). `ReactTextView.onDraw` invokes `DrawCommandSpan.onDraw` after `super.onDraw`, mirroring what `PreparedLayoutTextView.onDraw` already did. Without this, the new spans have no effect on the older view class, which is what some Text components on the new architecture still route through. ## Changelog: [GENERAL] [ADDED] - `textDecorationStyle: 'wavy'` for `` (see corresponding iOS PR for the iOS counterpart) [ANDROID] [ADDED] - Text decorations honor `textDecorationStyle` (`solid`, `double`, `dotted`, `dashed`, `wavy`) ## Test Plan: Rendered `` components with `textDecorationLine` set to `"underline"` or `"line-through"` and `textDecorationStyle` cycling through `solid` / `double` / `dotted` / `dashed` / `wavy`. On stock 0.85.2 every value renders as a solid line and `wavy` logs an `Unsupported value` warning; with this patch each style renders with the requested stroke geometry. Verified single-line and wrapped multi-line cases on an Android API 36 emulator: each visual line within a wrapped block receives its own correctly-styled decoration that starts and ends at the line's content boundaries. ```tsx Hello ``` --- .../react/views/text/ReactTextView.java | 19 ++ .../react/views/text/TextAttributeProps.kt | 25 ++- .../react/views/text/TextDecorationStyle.kt | 175 ++++++++++++++++++ .../react/views/text/TextLayoutManager.kt | 8 +- .../internal/span/ReactStrikethroughSpan.kt | 34 +++- .../text/internal/span/ReactUnderlineSpan.kt | 30 ++- .../renderer/attributedstring/conversions.h | 4 + .../renderer/attributedstring/primitives.h | 2 +- 8 files changed, 284 insertions(+), 13 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextDecorationStyle.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index a23e1d3d59db..6c3e4b8238a8 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -46,6 +46,7 @@ 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.ReactFragmentIndexSpan; import com.facebook.react.views.text.internal.span.ReactTagSpan; import com.facebook.yoga.YogaMeasureMode; @@ -213,6 +214,24 @@ protected void onDraw(Canvas canvas) { } super.onDraw(canvas); + + if (spanned != null) { + Layout layout = getLayout(); + if (layout != null) { + DrawCommandSpan[] drawSpans = + spanned.getSpans(0, spanned.length(), DrawCommandSpan.class); + if (drawSpans.length > 0) { + canvas.save(); + canvas.translate(getCompoundPaddingLeft(), getExtendedPaddingTop()); + for (DrawCommandSpan span : drawSpans) { + int start = spanned.getSpanStart(span); + int end = spanned.getSpanEnd(span); + span.onDraw(start, end, canvas, layout); + } + canvas.restore(); + } + } + } } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt index 45c8bec4c2b8..2fbf5dbbd02e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt @@ -92,6 +92,22 @@ public class TextAttributeProps private constructor() { public var isLineThroughTextDecorationSet: Boolean = false private set + /** + * Underline color. `Color.TRANSPARENT` (the default) means "fall back to + * the text color" so existing call sites that don't pass a value retain + * the prior behavior. Honored by `ReactUnderlineSpan` on API 29+. + */ + public var textDecorationColor: Int = android.graphics.Color.TRANSPARENT + private set + + /** + * CSS `text-decoration-style`. Defaults to `SOLID` so existing call + * sites retain the prior visual behavior. Honored by + * `ReactUnderlineSpan` and `ReactStrikethroughSpan`. + */ + internal var textDecorationStyle: TextDecorationStyle = TextDecorationStyle.SOLID + private set + private var includeFontPadding: Boolean = true public var accessibilityRole: AccessibilityRole? = null @@ -415,9 +431,10 @@ public class TextAttributeProps private constructor() { TA_KEY_LINE_HEIGHT -> result.lineHeight = entry.doubleValue.toFloat() TA_KEY_ALIGNMENT -> {} TA_KEY_BEST_WRITING_DIRECTION -> {} - TA_KEY_TEXT_DECORATION_COLOR -> {} + TA_KEY_TEXT_DECORATION_COLOR -> result.textDecorationColor = entry.intValue TA_KEY_TEXT_DECORATION_LINE -> result.setTextDecorationLine(entry.stringValue) - TA_KEY_TEXT_DECORATION_STYLE -> {} + TA_KEY_TEXT_DECORATION_STYLE -> + result.textDecorationStyle = TextDecorationStyle.fromString(entry.stringValue) TA_KEY_TEXT_SHADOW_RADIUS -> result.textShadowRadius = entry.doubleValue.toFloat() TA_KEY_TEXT_SHADOW_COLOR -> result.textShadowColor = entry.intValue TA_KEY_TEXT_SHADOW_OFFSET_DX -> result.textShadowOffsetDx = entry.doubleValue.toFloat() @@ -462,6 +479,10 @@ public class TextAttributeProps private constructor() { result.setFontVariant(getArrayProp(props, ViewProps.FONT_VARIANT)) result.includeFontPadding = getBooleanProp(props, ViewProps.INCLUDE_FONT_PADDING, true) result.setTextDecorationLine(getStringProp(props, ViewProps.TEXT_DECORATION_LINE)) + result.textDecorationColor = + getIntProp(props, "textDecorationColor", android.graphics.Color.TRANSPARENT) + result.textDecorationStyle = + TextDecorationStyle.fromString(getStringProp(props, "textDecorationStyle")) result.setTextShadowOffset( if (props.hasKey(PROP_SHADOW_OFFSET)) props.getMap(PROP_SHADOW_OFFSET) else null ) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextDecorationStyle.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextDecorationStyle.kt new file mode 100644 index 000000000000..ec7fdfcc15ff --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextDecorationStyle.kt @@ -0,0 +1,175 @@ +/* + * 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 + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.DashPathEffect +import android.graphics.Paint +import android.graphics.Path +import android.os.Build +import android.text.Layout +import kotlin.math.max +import kotlin.math.roundToInt + +/** + * Styles supported by the CSS `text-decoration-style` property, surfaced + * end-to-end by Fabric (see `TextAttributes::textDecorationStyle`). + */ +internal enum class TextDecorationStyle { + SOLID, + DOUBLE, + DOTTED, + DASHED, + WAVY; + + internal companion object { + @JvmStatic + fun fromString(value: String?): TextDecorationStyle = + when (value) { + "double" -> DOUBLE + "dotted" -> DOTTED + "dashed" -> DASHED + "wavy" -> WAVY + else -> SOLID + } + } +} + +/** + * Draws a horizontal decoration line between `x1` and `x2` at `y`, + * applying the requested CSS `text-decoration-style`. The caller is + * expected to have already configured `paint.color`, `paint.strokeWidth`, + * `paint.style = STROKE`, and `paint.isAntiAlias = true`, and to restore + * those after the call returns. The `paint.pathEffect` is saved and + * restored internally because dotted/dashed need to set it temporarily. + * + * Constants match Chromium/Blink's decoration_line_painter.cc so the + * visual rendering is consistent with what users see in Chrome on + * Android: + * - DOUBLE: center-to-center distance is `thickness + 1`. + * - WAVY: wavelength = `1 + 2 * round(2 * thickness + 0.5)`, + * controlPointDistance = `0.5 + round(3 * thickness + 0.5)`. + * One cubic Bezier per wavelength with both control points at the + * midpoint, one above and one below the y-axis. + */ +internal fun drawDecorationLine( + canvas: Canvas, + paint: Paint, + x1: Float, + x2: Float, + y: Float, + thickness: Float, + style: TextDecorationStyle, +) { + when (style) { + TextDecorationStyle.SOLID -> canvas.drawLine(x1, y, x2, y, paint) + TextDecorationStyle.DOUBLE -> { + // Center-to-center distance such that the visible gap between the + // top and bottom strokes (= gap - thickness) is 2 px regardless of + // stroke width. Blink renders with a 1 px gap, but with + // antialiasing that often reads as a single fat line; the wider + // gap keeps both strokes legible. + val gap = thickness + 2f + canvas.drawLine(x1, y, x2, y, paint) + canvas.drawLine(x1, y + gap, x2, y + gap, paint) + } + TextDecorationStyle.DOTTED, + TextDecorationStyle.DASHED -> { + val intervals = + if (style == TextDecorationStyle.DOTTED) floatArrayOf(thickness, thickness * 2f) + else floatArrayOf(thickness * 4f, thickness * 2f) + val savedEffect = paint.pathEffect + paint.pathEffect = DashPathEffect(intervals, 0f) + // `Canvas.drawLine` ignores `pathEffect`; draw the line as a Path + // so the dash intervals are honored. + val path = Path() + path.moveTo(x1, y) + path.lineTo(x2, y) + canvas.drawPath(path, paint) + paint.pathEffect = savedEffect + } + TextDecorationStyle.WAVY -> { + val clamped = max(1f, thickness) + val wavelength = 1f + 2f * (2f * clamped + 0.5f).roundToInt() + val cpDistance = 0.5f + (3f * clamped + 0.5f).roundToInt() + val path = Path() + path.moveTo(x1, y) + var x = x1 + // Loop while `x < x2` (not `x + wavelength <= x2`) so the wave + // continues through the final character (including trailing + // punctuation). The last cycle may extend a hair past the run, + // which reads as a natural underline trailer. + while (x < x2) { + val midX = x + wavelength / 2f + val endX = x + wavelength + // Two control points at the midpoint, one above (y - cp) and + // one below (y + cp). Produces an oscillating S-curve per + // wavelength, matching Chromium/Blink's wavy underline. + path.cubicTo(midX, y + cpDistance, midX, y - cpDistance, endX, y) + x = endX + } + canvas.drawPath(path, paint) + } + } +} + +/** + * Shared decoration drawing entry point used by [ReactUnderlineSpan] and + * [ReactStrikethroughSpan]. Computes a density-aware stroke thickness, + * sets up the paint, iterates the visible lines of the run, and delegates + * each line to [drawDecorationLine]. The caller-supplied [yOffsetForLine] + * computes the vertical position of the decoration line on each visible + * line of text (underline vs strikethrough being the only difference). + */ +internal inline fun drawSpannedDecoration( + start: Int, + end: Int, + canvas: Canvas, + layout: Layout, + color: Int, + style: TextDecorationStyle, + yOffsetForLine: (paint: Paint, baseline: Float, thickness: Float) -> Float, +) { + val paint = layout.paint + val savedColor = paint.color + val savedStrokeWidth = paint.strokeWidth + val savedStyle = paint.style + val savedAntiAlias = paint.isAntiAlias + val effectiveColor = if (color != Color.TRANSPARENT) color else savedColor + // Density-aware minimum so the decoration reads consistently across + // display densities (`paint.density` is the px-per-dp ratio). + val minThickness = 1.5f * paint.density + val thickness = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + max(paint.underlineThickness, minThickness) + } else { + max(paint.fontMetrics.descent * 0.1f, minThickness) + } + + paint.color = effectiveColor + paint.strokeWidth = thickness + paint.style = Paint.Style.STROKE + paint.isAntiAlias = true + + val startLine = layout.getLineForOffset(start) + val endLine = layout.getLineForOffset(end) + for (line in startLine..endLine) { + val baseline = layout.getLineBaseline(line).toFloat() + val x1 = + if (line == startLine) layout.getPrimaryHorizontal(start) else layout.getLineLeft(line) + val x2 = if (line == endLine) layout.getPrimaryHorizontal(end) else layout.getLineRight(line) + val y = yOffsetForLine(paint, baseline, thickness) + drawDecorationLine(canvas, paint, x1, x2, y, thickness, style) + } + + paint.color = savedColor + paint.strokeWidth = savedStrokeWidth + paint.style = savedStyle + paint.isAntiAlias = savedAntiAlias +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt index eacec697670d..41a286b847d1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt @@ -315,10 +315,10 @@ internal object TextLayoutManager { ) } if (textAttributes.isUnderlineTextDecorationSet) { - ops.add(SetSpanOperation(start, end, ReactUnderlineSpan())) + ops.add(SetSpanOperation(start, end, ReactUnderlineSpan(textAttributes.textDecorationColor, textAttributes.textDecorationStyle))) } if (textAttributes.isLineThroughTextDecorationSet) { - ops.add(SetSpanOperation(start, end, ReactStrikethroughSpan())) + ops.add(SetSpanOperation(start, end, ReactStrikethroughSpan(textAttributes.textDecorationColor, textAttributes.textDecorationStyle))) } if ( (textAttributes.textShadowOffsetDx != 0f || @@ -494,11 +494,11 @@ internal object TextLayoutManager { } if (fragment.props.isUnderlineTextDecorationSet) { - spannable.setSpan(ReactUnderlineSpan(), start, end, spanFlags) + spannable.setSpan(ReactUnderlineSpan(fragment.props.textDecorationColor, fragment.props.textDecorationStyle), start, end, spanFlags) } if (fragment.props.isLineThroughTextDecorationSet) { - spannable.setSpan(ReactStrikethroughSpan(), start, end, spanFlags) + spannable.setSpan(ReactStrikethroughSpan(fragment.props.textDecorationColor, fragment.props.textDecorationStyle), start, end, spanFlags) } if ( diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactStrikethroughSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactStrikethroughSpan.kt index 56cf633de69f..1004591604f7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactStrikethroughSpan.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactStrikethroughSpan.kt @@ -7,7 +7,35 @@ package com.facebook.react.views.text.internal.span -import android.text.style.StrikethroughSpan +import android.graphics.Canvas +import android.graphics.Color +import android.text.Layout +import com.facebook.react.views.text.TextDecorationStyle +import com.facebook.react.views.text.drawSpannedDecoration -/** Wraps [StrikethroughSpan] as a [ReactSpan]. */ -internal class ReactStrikethroughSpan : StrikethroughSpan(), ReactSpan +/** + * Draws a strikethrough whose color and style may differ from the text. + * The line is painted in `onDraw` after the layout renders its text. We + * do NOT extend [android.text.style.StrikethroughSpan] here: the + * framework's `Layout.draw` paints the strikethrough using `paint.color` + * with no field to override, so painting it ourselves is the only way to + * get a distinct color or non-solid style. + * + * `color == Color.TRANSPARENT` falls back to the text foreground color. + */ +internal class ReactStrikethroughSpan( + private val color: Int = Color.TRANSPARENT, + private val style: TextDecorationStyle = TextDecorationStyle.SOLID, +) : DrawCommandSpan() { + + override fun onDraw(start: Int, end: Int, canvas: Canvas, layout: Layout) { + drawSpannedDecoration(start, end, canvas, layout, color, style) { paint, baseline, _ -> + // Strikethrough sits near the x-height midline. `fontMetrics.ascent` + // is negative and `descent` is positive, so the sum / 2 gives a + // small negative offset from the baseline; the trailing `+ 1f` + // nudges it down to match the visual position users expect. + val fm = paint.fontMetrics + baseline + (fm.ascent + fm.descent) / 2f + 1f + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactUnderlineSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactUnderlineSpan.kt index 4cdc7c17978c..339b48fdf429 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactUnderlineSpan.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactUnderlineSpan.kt @@ -7,7 +7,31 @@ package com.facebook.react.views.text.internal.span -import android.text.style.UnderlineSpan +import android.graphics.Canvas +import android.graphics.Color +import android.text.Layout +import com.facebook.react.views.text.TextDecorationStyle +import com.facebook.react.views.text.drawSpannedDecoration -/** Wraps [UnderlineSpan] as a [ReactSpan]. */ -internal class ReactUnderlineSpan : UnderlineSpan(), ReactSpan +/** + * Draws an underline whose color and style may differ from the text. The + * underline is painted in `onDraw` (after the layout renders its text) so + * it lands on top of any descenders. We do NOT extend + * [android.text.style.UnderlineSpan] here: the framework's `Layout.draw` + * reads `paint.color` for underline color regardless of + * `paint.underlineColor`, so painting it ourselves is the only way to get + * a distinct color or non-solid style. + * + * `color == Color.TRANSPARENT` falls back to the text foreground color. + */ +internal class ReactUnderlineSpan( + private val color: Int = Color.TRANSPARENT, + private val style: TextDecorationStyle = TextDecorationStyle.SOLID, +) : DrawCommandSpan() { + + override fun onDraw(start: Int, end: Int, canvas: Canvas, layout: Layout) { + drawSpannedDecoration(start, end, canvas, layout, color, style) { _, baseline, thickness -> + baseline + thickness + 1f + } + } +} diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h index 331e2019338a..faa09c1dbffa 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h @@ -916,6 +916,8 @@ inline void fromRawValue(const PropsParserContext &context, const RawValue &valu result = TextDecorationStyle::Dotted; } else if (string == "dashed") { result = TextDecorationStyle::Dashed; + } else if (string == "wavy") { + result = TextDecorationStyle::Wavy; } else { LOG(ERROR) << "Unsupported TextDecorationStyle value: " << string; react_native_expect(false); @@ -941,6 +943,8 @@ inline std::string toString(const TextDecorationStyle &textDecorationStyle) return "dotted"; case TextDecorationStyle::Dashed: return "dashed"; + case TextDecorationStyle::Wavy: + return "wavy"; } LOG(ERROR) << "Unsupported TextDecorationStyle value"; diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/primitives.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/primitives.h index d23104518c74..459bbcf632c2 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/primitives.h @@ -134,7 +134,7 @@ enum class LineBreakMode { enum class TextDecorationLineType { None, Underline, Strikethrough, UnderlineStrikethrough }; -enum class TextDecorationStyle { Solid, Double, Dotted, Dashed }; +enum class TextDecorationStyle { Solid, Double, Dotted, Dashed, Wavy }; enum class TextTransform { None, From 676ed17a58bc9e8fcd76e7b44cd7915d99f993c9 Mon Sep 17 00:00:00 2001 From: Evan Jacobs Date: Mon, 11 May 2026 13:11:40 -0400 Subject: [PATCH 2/2] fix(android,ios): rebase onto CanvasEffectSpan rename + iOS Wavy stub Three CI breakages were introduced by changes that landed on `main` between the original PR push and now: 1. PR #56705 renamed `DrawCommandSpan` to `CanvasEffectSpan` and dropped the `ReactSpan` / `UpdateAppearance` interfaces from the base class. `ReactUnderlineSpan` and `ReactStrikethroughSpan` were extending the old name; rename them and re-declare `ReactSpan` so they remain valid `SetSpanOperation` arguments. `ReactTextView.onDraw` updated to import the new name. 2. Adding `Wavy` to `facebook::react::TextDecorationStyle` (this PR) left `RCTNSUnderlineStyleFromTextDecorationStyle` non-exhaustive, tripping `-Werror,-Wreturn-type` on iOS builds. Add a `Wavy` case that falls back to a solid underline (the actual wavy rendering ships in companion PR #56769). 3. `validate_cxx_api_snapshots` flagged the missing `Wavy` entry in all six snapshots under `scripts/cxx-api/api-snapshots/`. Regenerate. No behavior change beyond what was already in the feature commit; this is purely "rebase the implementation onto current `main`." --- .../java/com/facebook/react/views/text/ReactTextView.java | 8 ++++---- .../views/text/internal/span/ReactStrikethroughSpan.kt | 2 +- .../react/views/text/internal/span/ReactUnderlineSpan.kt | 2 +- .../textlayoutmanager/RCTTextPrimitivesConversions.h | 2 ++ scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api | 1 + scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api | 1 + scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api | 1 + scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api | 1 + scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api | 1 + scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api | 1 + 10 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index 6c3e4b8238a8..22b509428e87 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -46,7 +46,7 @@ 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.CanvasEffectSpan; import com.facebook.react.views.text.internal.span.ReactFragmentIndexSpan; import com.facebook.react.views.text.internal.span.ReactTagSpan; import com.facebook.yoga.YogaMeasureMode; @@ -218,12 +218,12 @@ protected void onDraw(Canvas canvas) { if (spanned != null) { Layout layout = getLayout(); if (layout != null) { - DrawCommandSpan[] drawSpans = - spanned.getSpans(0, spanned.length(), DrawCommandSpan.class); + CanvasEffectSpan[] drawSpans = + spanned.getSpans(0, spanned.length(), CanvasEffectSpan.class); if (drawSpans.length > 0) { canvas.save(); canvas.translate(getCompoundPaddingLeft(), getExtendedPaddingTop()); - for (DrawCommandSpan span : drawSpans) { + for (CanvasEffectSpan span : drawSpans) { int start = spanned.getSpanStart(span); int end = spanned.getSpanEnd(span); span.onDraw(start, end, canvas, layout); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactStrikethroughSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactStrikethroughSpan.kt index 1004591604f7..48364cd7b059 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactStrikethroughSpan.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactStrikethroughSpan.kt @@ -26,7 +26,7 @@ import com.facebook.react.views.text.drawSpannedDecoration internal class ReactStrikethroughSpan( private val color: Int = Color.TRANSPARENT, private val style: TextDecorationStyle = TextDecorationStyle.SOLID, -) : DrawCommandSpan() { +) : CanvasEffectSpan(), ReactSpan { override fun onDraw(start: Int, end: Int, canvas: Canvas, layout: Layout) { drawSpannedDecoration(start, end, canvas, layout, color, style) { paint, baseline, _ -> diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactUnderlineSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactUnderlineSpan.kt index 339b48fdf429..28f3b54e3a13 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactUnderlineSpan.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactUnderlineSpan.kt @@ -27,7 +27,7 @@ import com.facebook.react.views.text.drawSpannedDecoration internal class ReactUnderlineSpan( private val color: Int = Color.TRANSPARENT, private val style: TextDecorationStyle = TextDecorationStyle.SOLID, -) : DrawCommandSpan() { +) : CanvasEffectSpan(), ReactSpan { override fun onDraw(start: Int, end: Int, canvas: Canvas, layout: Layout) { drawSpannedDecoration(start, end, canvas, layout, color, style) { _, baseline, thickness -> diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextPrimitivesConversions.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextPrimitivesConversions.h index 1687206f1c29..5c6d1c99f2a0 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextPrimitivesConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextPrimitivesConversions.h @@ -110,6 +110,8 @@ inline static NSUnderlineStyle RCTNSUnderlineStyleFromTextDecorationStyle( return NSUnderlineStylePatternDash | NSUnderlineStyleSingle; case facebook::react::TextDecorationStyle::Dotted: return NSUnderlineStylePatternDot | NSUnderlineStyleSingle; + case facebook::react::TextDecorationStyle::Wavy: + return NSUnderlineStyleSingle; } } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 54d994781de4..035b05379bb3 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -6505,6 +6505,7 @@ enum facebook::react::TextDecorationStyle { Dotted, Double, Solid, + Wavy, } enum facebook::react::TextTransform { diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 6a0a48343467..c06e0d7e76d1 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -6496,6 +6496,7 @@ enum facebook::react::TextDecorationStyle { Dotted, Double, Solid, + Wavy, } enum facebook::react::TextTransform { diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index bb3a74b3522e..402e690327c0 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -9073,6 +9073,7 @@ enum facebook::react::TextDecorationStyle { Dotted, Double, Solid, + Wavy, } enum facebook::react::TextInputAccessoryVisibilityMode { diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 53f03430fc4b..78421dd8cee9 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -9064,6 +9064,7 @@ enum facebook::react::TextDecorationStyle { Dotted, Double, Solid, + Wavy, } enum facebook::react::TextInputAccessoryVisibilityMode { diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index bcfb19940fae..bfe05b44d040 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -4856,6 +4856,7 @@ enum facebook::react::TextDecorationStyle { Dotted, Double, Solid, + Wavy, } enum facebook::react::TextTransform { diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 53c14ca18d22..299bfc88a95e 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -4847,6 +4847,7 @@ enum facebook::react::TextDecorationStyle { Dotted, Double, Solid, + Wavy, } enum facebook::react::TextTransform {