fix(android,ios): honor textDecorationColor and textDecorationStyle on Text#56748
Open
quantizor wants to merge 3 commits intofacebook:mainfrom
Open
fix(android,ios): honor textDecorationColor and textDecorationStyle on Text#56748quantizor wants to merge 3 commits intofacebook:mainfrom
quantizor wants to merge 3 commits intofacebook:mainfrom
Conversation
Android's `Layout.draw` paints the underline produced by `setUnderlineText(true)` using `paint.color`, ignoring `paint.underlineColor` on all API levels. This caused `textDecorationColor` to be silently dropped on Android. Refactor `ReactUnderlineSpan` to extend `DrawCommandSpan` and paint the underline itself in `onDraw`, falling back to the text color when no color was specified. Thread the color through `TextAttributeProps` (both MapBuffer and ReadableMap ingestion paths) and `TextLayoutManager`. Add `DrawCommandSpan` invocation to `ReactTextView.onDraw`, mirroring the existing `PreparedLayoutTextView` behavior so both text view classes honor custom-drawing spans. ## Changelog [ANDROID] [FIXED] - Text underlines honor `textDecorationColor` ## Test Plan Render a Text component with `textDecorationColor` set to a value distinct from the text color; the underline now renders in the specified color rather than the text color. Verified on Android API 36 emulator.
ac5bbab to
1a8cbda
Compare
quantizor
added a commit
to quantizor/react-native
that referenced
this pull request
May 11, 2026
Iterations on the textDecorationStyle implementation that landed in PR facebook#56748, based on visual comparison with Chrome / Safari and side-by-side testing of the two platforms. iOS: - Wavy: thickness divisor relaxed from `fontSize / 8` to `fontSize / 12` and control-point distance multiplier halved (`1.5 * thickness + 0.5` vs Blink's literal `3 * thickness + 0.5`). At iOS point sizes the literal Blink amplitude renders as a very pronounced wave; the dialed- back values read as a clear-but-subtle browser-style wave. - Dotted: switched from UIKit's `NSUnderlineStylePatternDot` (which doesn't match browser geometry) to a custom CG path with a zero-length dash + round line caps, producing actual circular dots at `2 * thickness` spacing. - Dashed: switched from UIKit's `NSUnderlineStylePatternDash` to custom CG path with `[2 * thickness, thickness]` intervals — short rectangular dashes with a tight gap, closer to Safari's geometry than UIKit's default. - The custom decoration attribute (formerly `RCTWavyDecorationAttributeName`) is now `RCTCustomDecorationAttributeName` and carries a `style` key so the same drawing pipeline handles wavy + dotted + dashed. Cross-platform: - Wavy drawing loop now iterates `while x < x2` instead of `while x + wavelength <= x2`, so the final cycle continues through the last character (including trailing punctuation). Previously a trailing period could be visually uncovered when the run width was not an integer multiple of the wavelength. ## Changelog: [IOS] [CHANGED] - Wavy, dotted, and dashed text decorations render with custom CoreGraphics paths instead of UIKit pattern bits, matching browser geometry more closely [GENERAL] [FIXED] - Wavy underline / strikethrough now extends through the final character of the run, including trailing punctuation ## Test Plan: Side-by-side comparison on Android API 36 emulator and iPhone 17 sim (iOS 26.4) of a `<Text>` with `textDecorationLine="underline"` and `textDecorationStyle` cycling through `wavy` / `dotted` / `dashed`, verified against Chrome (Android view) and Safari (iOS view) rendering of the same CSS. Trailing periods now fall under the wavy stroke on both platforms.
Contributor
|
@quantizor would it be too hard to split this into 3 separate PRs? 🙏 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
textDecorationColoris declared on bothTextStyleIOSandTextStyleAndroidin the public types, but on Android the value has no visible effect: the underline always paints in the text's foreground color. iOS honors color correctly viaNSUnderlineColorAttributeName. Separately,textDecorationStyle: 'wavy'is silently dropped on both platforms (Fabric's C++ enum doesn't includeWavy, and the JSStyleSheettypes describe it as supported).This PR closes both gaps cross-platform:
Color (Android): Android's
Layout.drawpaints the underline produced bysetUnderlineText(true)usingpaint.color, ignoringpaint.underlineColoron every API level. The same applies to strikethrough.ReactUnderlineSpanandReactStrikethroughSpannow extendDrawCommandSpanand paint the decoration themselves inonDrawviaCanvas.drawLine/Canvas.drawPath, so the requested color actually reaches the paint.Style (Android): Wires
textDecorationStylethrough the existing C++ → Kotlin pipeline (TA_KEY_TEXT_DECORATION_STYLEwas a no-op handler). Implementssolid,double,dotted,dashed, andwavy. The wavy curve uses Chromium/Blink's formula fromdecoration_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.Style (iOS, wavy): UIKit's
NSUnderlineStylehas no native wavy value, so Wavy ranges are tagged with a customRCTWavyDecorationAttributeName(storing the line kinds + stroke color) inRCTAttributedTextUtils.mmand painted byRCTTextLayoutManager.mmafterdrawGlyphsForGlyphRange:using WebKit's formula fromSource/WebCore/style/InlineTextBoxStyle.cpp(controlPointDistance = fontSize * 1.5 / 16,step = fontSize / 4.5, one cubic Bezier per wavelength, control points at the midpoint above and below the y-axis). The other four styles continue to use UIKit's nativeNSUnderlineStylepattern bits.ReactTextView (Android): invokes
DrawCommandSpan.onDrawaftersuper.onDraw, mirroring whatPreparedLayoutTextView.onDrawalready 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.C++ enum:
TextDecorationStyle::Wavyadded toprimitives.hand theconversions.h(de)serialization, so thewavyJS value flows through Fabric instead of being rejected with anUnsupported valuelog.Related: #4579 (2015) flagged the color gap.
Changelog:
[ANDROID] [FIXED] - Text underlines and strikethroughs honor
textDecorationColor[GENERAL] [ADDED] -
textDecorationStyle: 'wavy'(rendered natively on both Android and iOS using each platform's reference browser-engine formula)[ANDROID] [ADDED] - Text decorations honor
textDecorationStyle(solid,double,dotted,dashed,wavy)Test Plan:
Rendered
<Text>components withtextDecorationLineset to"underline"or"line-through",textDecorationStylecycling throughsolid/double/dotted/dashed/wavy, andtextDecorationColorset to a value distinct from the foreground color. On stock 0.85.2 the decorations render in the text color with no style variation (andwavyis dropped); with this patch each style renders in the specified color with the requested stroke geometry. Verified single-line and wrapped multi-line cases on an Android API 36 emulator and iPhone 17 sim (iOS 26.4): each visual line within a wrapped block receives its own correctly-colored decoration that starts and ends at the line's content boundaries.