feat(ios): honor textDecorationStyle on Text decorations#56769
Open
quantizor wants to merge 2 commits into
Open
feat(ios): honor textDecorationStyle on Text decorations#56769quantizor wants to merge 2 commits into
quantizor wants to merge 2 commits into
Conversation
`textDecorationStyle` is declared on `TextStyleIOS` in the public types
but `wavy` is silently dropped (Fabric's C++ enum doesn't include
`Wavy`, and UIKit's `NSUnderlineStyle` has no native wavy pattern bit).
This PR closes the gap by adding `TextDecorationStyle::Wavy` to the
shared Fabric primitives / conversions and rendering wavy / dotted /
dashed decorations with custom Core Graphics paths instead of UIKit
pattern bits.
Implementation:
- Wavy ranges are tagged with a custom
`RCTCustomDecorationAttributeName` (storing the line kinds, stroke
color, and style key) in `RCTAttributedTextUtils.mm` and painted by
`RCTTextLayoutManager.mm` after `drawGlyphsForGlyphRange:`.
- Wavy uses an adaptation of WebKit's formula from
`Source/WebCore/style/InlineTextBoxStyle.cpp`
(`controlPointDistance = thickness * 1.5 + 0.5`, one cubic Bezier
per wavelength, control points at the midpoint above and below the
y-axis). At iOS point sizes the literal Blink amplitude renders as
a very pronounced wave because Core Graphics paints in points (not
device pixels), so the constants are dialed back to read as a
clear-but-subtle browser-style wave at typical text sizes.
- Dotted uses a custom CG path with a zero-length dash + round line
caps, producing actual circular dots at `2 * thickness` spacing
(UIKit's `NSUnderlineStylePatternDot` does not match browser
geometry on iOS).
- Dashed uses a custom CG path with `[2 * thickness, thickness]`
intervals — short rectangular dashes with a tight gap, closer to
Safari's geometry than UIKit's default `NSUnderlineStylePatternDash`.
- Solid and double continue to use UIKit's native `NSUnderlineStyle`
pattern bits.
- The wavy 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).
The shared C++ enum addition unblocks the same value on Android (see
companion PR).
## Changelog:
[IOS] [ADDED] - `textDecorationStyle: 'wavy'` for `<Text>` (custom CG path)
[IOS] [CHANGED] - `textDecorationStyle: 'dotted'` and `'dashed'` for `<Text>` render with custom CoreGraphics paths instead of UIKit pattern bits, matching browser geometry more closely
## Test Plan:
Side-by-side comparison on iPhone 17 sim (iOS 26.4) of a `<Text>` with
`textDecorationLine="underline"` and `textDecorationStyle` cycling
through `solid` / `double` / `dotted` / `dashed` / `wavy`, verified
against Safari rendering of the same CSS. Trailing periods now fall
under the wavy stroke. Verified with `textDecorationColor` set
distinct from the foreground color.
```tsx
<Text style={{
color: 'black',
textDecorationLine: 'underline',
textDecorationStyle: 'wavy',
textDecorationColor: '#ff00aa',
}}>
Hello
</Text>
```
This was referenced May 11, 2026
This was referenced May 11, 2026
|
@CalixTang has imported this pull request. If you are a Meta employee, you can view this in D104680636. |
The `feat(ios): honor textDecorationStyle` commit added `Wavy` to the shared `facebook::react::TextDecorationStyle` enum but the C++ API snapshots under `scripts/cxx-api/api-snapshots/` weren't regenerated. CI's `validate_cxx_api_snapshots` job flagged the divergence across all six snapshot files (Android Debug/Release, Apple Debug/Release, Common Debug/Release). No code or behavior change.
|
Warning JavaScript API change detected This PR commits an update to
This change was flagged as: |
quantizor
added a commit
to quantizor/react-native
that referenced
this pull request
May 11, 2026
Three CI breakages were introduced by changes that landed on `main` between the original PR push and now: 1. PR facebook#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 facebook#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`."
quantizor
added a commit
to quantizor/react-native
that referenced
this pull request
May 11, 2026
Three CI breakages were introduced by changes that landed on `main` between the original PR push and now: 1. PR facebook#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 facebook#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`."
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:
textDecorationStyleis declared onTextStyleIOSin the public types butwavyis silently dropped: Fabric's C++ enum doesn't includeWavy, and UIKit'sNSUnderlineStylehas no native wavy pattern bit. Separately,dottedanddashedmap toNSUnderlineStylePatternDot/NSUnderlineStylePatternDashwhich don't match browser geometry on iOS.This PR adds
TextDecorationStyle::Wavyto the shared Fabric primitives / conversions (also unblocks the same value on Android, see companion PR #56768) and renders wavy / dotted / dashed decorations with custom Core Graphics paths.Implementation:
RCTCustomDecorationAttributeName(storing the line kinds, stroke color, and style key) inRCTAttributedTextUtils.mmand painted byRCTTextLayoutManager.mmafterdrawGlyphsForGlyphRange:. Wavy uses an adaptation of WebKit's formula fromSource/WebCore/style/InlineTextBoxStyle.cpp(controlPointDistance = thickness * 1.5 + 0.5, one cubic Bezier per wavelength, control points at the midpoint above and below the y-axis). At iOS point sizes the literal Blink amplitude renders as a very pronounced wave because Core Graphics paints in points (not device pixels), so the constants are dialed back to read as a clear-but-subtle browser-style wave at typical text sizes.2 * thicknessspacing.[2 * thickness, thickness]intervals — short rectangular dashes with a tight gap, closer to Safari's geometry than UIKit's default.NSUnderlineStylepattern bits, so this PR does not touch the long-standing iOS Arial+bold solid-underline rendering bug tracked in [iOS] rendering Arial, underline and bold text produce bad underlining in specific case fontSize configuration #53935.while x < x2so 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).Companion PRs (independent, also targeting
main):TextDecorationStyle::Wavyenum addition; whichever lands first leaves the other with a trivial conflict to resolve.Changelog:
[IOS] [ADDED] -
textDecorationStyle: 'wavy'for<Text>(custom CoreGraphics path)[IOS] [CHANGED] -
textDecorationStyle: 'dotted'and'dashed'for<Text>render with custom CoreGraphics paths instead of UIKit pattern bits, matching browser geometry more closelyTest Plan:
Side-by-side comparison on iPhone 17 sim (iOS 26.4) of a
<Text>withtextDecorationLine="underline"andtextDecorationStylecycling throughsolid/double/dotted/dashed/wavy, verified against Safari rendering of the same CSS. Trailing periods now fall under the wavy stroke. Verified withtextDecorationColorset distinct from the foreground color.