Skip to content

fix(ios,android): polish wavy/dotted/dashed text decoration rendering#56749

Open
quantizor wants to merge 4 commits intofacebook:mainfrom
quantizor:fix-text-decoration-rendering-polish
Open

fix(ios,android): polish wavy/dotted/dashed text decoration rendering#56749
quantizor wants to merge 4 commits intofacebook:mainfrom
quantizor:fix-text-decoration-rendering-polish

Conversation

@quantizor
Copy link
Copy Markdown

Summary:

Polish pass on the textDecorationStyle cross-platform implementation from #56748, based on visual comparison with Chrome (Android) and Safari (iOS) rendering of the same CSS.

This PR is stacked on top of #56748. Until #56748 merges, the diff here includes that PR's commits as well; the polish-specific changes are in the single commit on top.

iOS rendering:

  • Wavy: thickness divisor relaxed from fontSize / 8 to fontSize / 12 and the 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 because Core Graphics paints in points, not device pixels — 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 on iOS) 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 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.
  • The custom-attribute name (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.

quantizor added 3 commits May 10, 2026 23:50
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.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 11, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label 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.
@quantizor quantizor force-pushed the fix-text-decoration-rendering-polish branch from 5898e8f to 7bf5142 Compare May 11, 2026 06:30
@fabriziocucci
Copy link
Copy Markdown
Contributor

@quantizor would it be too hard to split this into 2 separate PRs? 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants