Description
Description
Arabic glyphs whose ink extends past their advance width (alef-wasla ٱ, alef-madda آ, lam-alef ligatures) are clipped at the leading (right) edge — but only when the glyph's word starts a line. Mid-line instances render correctly. iOS is unaffected.
This is the same defect family as #32645 (open since 2021). We spent a multi-day investigation and can supply the full elimination matrix; summary below.
What we established
- Padding cannot fix it: tested to a 24pt ceiling on a line with 2.23pt of measured overhang ink — still clipped. The clip is internal to line layout, not box geometry.
- Every directional lever (direction:'rtl' on Text, on ancestor View, textAlign:'right', 'justify') engages the clipping path. Removing all directional styling renders the ink but loses right-flush (alignment follows component layout direction, not paragraph bidi).
- Measured ink (fontkit, shaped runs via font.layout(), 26pt): Amiri worst-case leading overhang 2.23pt; KFGQPC Hafs 1.04pt; Noto Naskh 0.49pt. Fonts with ink inside advances mask the bug — it is not rare, just font-dependent.
- 8347cc4 (0.83.2) explicitly scopes its two-pass bounds fix to unconstrained/AT_MOST text ("Text with EXACTLY width mode is unaffected") — full-width paragraphs measure EXACTLY and never receive it.
The fix that exists but is never called
Android 15 (API 35) added StaticLayout.Builder#setUseBoundsForWidth and #setShiftDrawingOffsetForStartOverhang — the latter is documented as shifting the drawing offset so start-side overhang ink renders instead of clipping. In RN 0.86.2:
- TextLayoutManager.kt has exactly ONE StaticLayout.Builder site (buildLayout, ~line 674, the funnel for both measurement and Fabric layout creation). Neither setter is applied there.
- TextLayoutManager.kt:108-118 already contains a lazily-cached reflection Method handle for setUseBoundsForWidth — with zero call sites anywhere in the tree. The plumbing exists; it was never connected.
- App-side workarounds are impossible under Fabric: with enablePreparedTextLayout active (observed enabled under Expo SDK 57), all text renders via PreparedLayoutTextView from the precomputed layout; runtime probing found reactTextViews=0 of 31 text views, so no view-level setter can reach the drawn layout.
Proposed fix (verified non-executing in-app; we could not build RN from source to device-test it):
// TextLayoutManager.kt, buildLayout(), before builder.build():
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
builder.setUseBoundsForWidth(true)
builder.setShiftDrawingOffsetForStartOverhang(true)
}
Reproducer
https://github.com/muhasabahhub/android-react-native-arabic
Environment
| react-native | 0.86.2 (new architecture / Fabric, newArchEnabled: true) |
| expo | ~57.0.15 |
| Font | Amiri Regular, bundled at assets/fonts/Amiri-Regular.ttf (SIL OFL 1.1, assets/fonts/OFL.txt) |
| Reproduces on | Android 15 (API 35) and Android 16 (API 36) |
| Does not reproduce on | iOS |
The font matters: Amiri's alif-madda has ~2.2pt of ink past its advance box at
fontSize: 26. A low-overhang face such as Noto Naskh (~0.5pt) hides the bug
without fixing it.
Screenshots
Steps to reproduce
rtl-ink-clip-repro
Minimal reproduction: Arabic leading ink is clipped at the start of an RTL line on Android.
Text 1 clips the alif-madda's leading ink at line start (Android 15+, RN 0.86/Fabric).
Text 2 renders the ink but flushes left.
iOS renders both correctly.
Repro steps
npm install
npx expo run:android — a dev build is required; this cannot be seen in Expo Go.
- Look at the two tinted paragraphs. The tint is each
<Text>'s own background,
so the coloured rectangle is the view's box.
- Find the word beginning with U+0622 (alif madda) at the start of the second
line of each paragraph. The console prints the first codepoint of every
rendered line via onTextLayout, e.g. [text1-direction-rtl] line 2 starts U+0622
— use it to confirm the word is positioned correctly.
- If U+0622 does not land at the start of line 2 on your screen width, adjust
FILLER_COUNT_BEFORE in App.tsx and repeat.
Both <Text> elements are full-width with zero horizontal padding, deliberately:
any side padding widens the view's bounds and masks the clipping.
Expected
The alif-madda's madda stroke renders in full, inside the tinted box, at the
right-hand (reading-start) edge of line 2 — as it does on iOS.
Actual
- Text 1 (
style={{ direction: 'rtl' }}): the paragraph is correctly
right-aligned, and the madda stroke is cut off vertically at the tint's
right edge. The ink that overhangs the glyph's advance box falls outside
the text view's bounds and is not drawn.
- Text 2 (no
direction style, string prefixed with U+200F RLM): the same
glyph renders complete, because the paragraph is flushed to the left and
the leading edge is no longer at the view's boundary — so the ink has room.
This is not a usable workaround: the paragraph is now left-aligned.
Text 2 is included to show that the clipping is a function of where the line
starts relative to the view's bounds, not of the glyph or the font.
Likely cause
ReactAndroid/.../views/text/TextLayoutManager.kt builds every layout at a
single StaticLayout.Builder.obtain(...) site and sets neither of the two
Android 15 APIs intended for exactly this case:
.setUseBoundsForWidth(true)
.setShiftDrawingOffsetForStartOverhang(true)
Notably, RN 0.86.2 already carries a lazily cached reflection handle for
setUseBoundsForWidth (TextLayoutManager.kt:108-118, with the comment
"Reflection is needed because some internal targets compile against an SDK
older than 35") — but it has zero call sites, and no feature flag guards
it. The API is reached for and then never used.
ReactTextView.onDraw ends in super.onDraw(canvas), i.e. the view draws the
Layout that TextView.makeNewLayout() builds for itself, so the equivalent
TextView setters at attach time are a second possible site.
Likely cause
ReactAndroid/.../views/text/TextLayoutManager.kt builds every layout at a
single StaticLayout.Builder.obtain(...) site and sets neither of the two
Android 15 APIs intended for exactly this case:
.setUseBoundsForWidth(true)
.setShiftDrawingOffsetForStartOverhang(true)
Notably, RN 0.86.2 already carries a lazily cached reflection handle for
setUseBoundsForWidth (TextLayoutManager.kt:108-118, with the comment
"Reflection is needed because some internal targets compile against an SDK
older than 35") — but it has zero call sites, and no feature flag guards
it. The API is reached for and then never used.
ReactTextView.onDraw ends in super.onDraw(canvas), i.e. the view draws the
Layout that TextView.makeNewLayout() builds for itself, so the equivalent
TextView setters at attach time are a second possible site.
React Native Version
0.86.2
Affected Platforms
Runtime - Android
Output of npx @react-native-community/cli info
System:
OS: Windows 11 10.0.26200
CPU: (22) x64 Intel(R) Core(TM) Ultra 7 155H
Memory: 2.23 GB / 15.59 GB
Binaries:
Node:
version: 24.13.1
path: C:\Program Files\nodejs\node.EXE
Yarn: Not Found
npm:
version: 11.19.0
path: C:\Program Files\nodejs\npm.CMD
Watchman: Not Found
SDKs:
Android SDK:
API Levels:
- "34"
- "35"
- "36"
Build Tools:
- 33.0.1
- 34.0.0
- 35.0.0
- 36.0.0
System Images:
- android-35 | Google Play Intel x86_64 Atom
Android NDK: Not Found
Windows SDK:
AllowDevelopmentWithoutDevLicense: Enabled
AllowAllTrustedApps: Enabled
IDEs:
Android Studio: AI-253.29346.138.2531.14876573
Visual Studio:
- 17.14.37314.3 (Visual Studio Community 2022)
Languages:
Java:
version: 23.0.1
path: C:\Program Files\Common Files\Oracle\Java\javapath\javac.EXE
Ruby: Not Found
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 19.2.3
wanted: 19.2.3
react-native:
installed: 0.86.2
wanted: 0.86.2
react-native-windows: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: Not found
newArchEnabled: Not found
iOS:
hermesEnabled: Not found
newArchEnabled: Not found
- newArchEnabled: true — set in app.json, so this is Fabric.
- Hermes is on (Expo SDK 57 default).
Stacktrace or Logs
--------- beginning of main
08-20 10:56:26.349 17466 17466 I INKFLAG : probe[requestLayout+invalidate]: sdk=36 textViews=83 reactTextViews=83
08-20 10:56:26.350 17466 17466 I INKFLAG : found textview, text="11/15" class=ReactTextView layout=StaticLayout w=108
08-20 10:56:26.350 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.350 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.350 17466 17466 I INKFLAG : found textview, text="" class=ReactTextView layout=BoringLayout w=72
08-20 10:56:26.350 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.350 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.350 17466 17466 I INKFLAG : found textview, text="" class=ReactTextView layout=BoringLayout w=72
08-20 10:56:26.350 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.350 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.350 17466 17466 I INKFLAG : found textview, text="1×" class=ReactTextView layout=StaticLayout w=43
08-20 10:56:26.350 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.350 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.350 17466 17466 I INKFLAG : found textview, text="لَا إِلَهَ إِلَّا ال" class=ReactTextView layout=StaticLayout w=984
08-20 10:56:26.350 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.350 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.350 17466 17466 I INKFLAG : found textview, text="None has the right t" class=ReactTextView layout=StaticLayout w=984
08-20 10:56:26.350 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.350 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.350 17466 17466 I INKFLAG : found textview, text="TRANSLITERATION" class=ReactTextView layout=StaticLayout w=371
08-20 10:56:26.350 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.350 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.350 17466 17466 I INKFLAG : found textview, text="" class=ReactTextView layout=BoringLayout w=48
08-20 10:56:26.350 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.350 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.350 17466 17466 I INKFLAG : found textview, text="VIRTUE & REFERENCE" class=ReactTextView layout=StaticLayout w=428
08-20 10:56:26.350 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.350 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.350 17466 17466 I INKFLAG : found textview, text="" class=ReactTextView layout=BoringLayout w=48
08-20 10:56:26.350 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.350 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.350 17466 17466 I INKFLAG : found textview, text="R" class=ReactTextView layout=StaticLayout w=31
08-20 10:56:26.350 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.350 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.350 17466 17466 I INKFLAG : found textview, text="T" class=ReactTextView layout=StaticLayout w=31
08-20 10:56:26.350 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.350 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.350 17466 17466 I INKFLAG : found textview, text="—" class=ReactTextView layout=StaticLayout w=36
08-20 10:56:26.350 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text="ٱللَّهُ لَآ إِلَٰهَ" class=ReactTextView layout=StaticLayout w=984
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text="A" class=ReactTextView layout=StaticLayout w=21
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text="ٱللَّهُ لَآ إِلَٰه" class=ReactTextView layout=StaticLayout w=668
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text="L" class=ReactTextView layout=StaticLayout w=17
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text=" ٱللَّهُ لَآ إِلَٰ" class=ReactTextView layout=StaticLayout w=558
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text=" ٱلۡقَيُّومُۚ لَا ت" class=ReactTextView layout=StaticLayout w=935
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text=" ٱلسَّمَٰوَٰتِ وَمَ" class=ReactTextView layout=StaticLayout w=726
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text=" ٱلَّذِي يَشۡفَعُ ع" class=ReactTextView layout=StaticLayout w=697
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text=" يَعۡلَمُ مَا بَيۡن" class=ReactTextView layout=StaticLayout w=851
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text=" يُحِيطُونَ بِشَيۡء" class=ReactTextView layout=StaticLayout w=984
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text=" ٱلسَّمَٰوَٰتِ وَٱل" class=ReactTextView layout=StaticLayout w=984
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text=" ٱلۡعَلِيُّ ٱلۡعَظِ" class=ReactTextView layout=StaticLayout w=329
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text="M" class=ReactTextView layout=StaticLayout w=28
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text="1×" class=ReactTextView layout=StaticLayout w=43
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text="آية الكرسي" class=ReactTextView layout=StaticLayout w=984
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text="Ayatul Kursi" class=ReactTextView layout=StaticLayout w=243
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
08-20 10:56:26.351 17466 17466 I INKFLAG : nudge ran: requestLayout+invalidate
08-20 10:56:26.351 17466 17466 I INKFLAG : found textview, text="Al-Baqarah · Ayah 25" class=ReactTextView layout=StaticLayout w=381
08-20 10:56:26.351 17466 17466 I INKFLAG : setters applied (useBoundsForWidth + shiftStartOverhang)
LAG : probe[setText(getText())]: DONE applied=83
MANDATORY Reproducer
https://github.com/muhasabahhub/android-react-native-arabic
Screenshots and Videos

Description
Description
Arabic glyphs whose ink extends past their advance width (alef-wasla ٱ, alef-madda آ, lam-alef ligatures) are clipped at the leading (right) edge — but only when the glyph's word starts a line. Mid-line instances render correctly. iOS is unaffected.
This is the same defect family as #32645 (open since 2021). We spent a multi-day investigation and can supply the full elimination matrix; summary below.
What we established
The fix that exists but is never called
Android 15 (API 35) added StaticLayout.Builder#setUseBoundsForWidth and #setShiftDrawingOffsetForStartOverhang — the latter is documented as shifting the drawing offset so start-side overhang ink renders instead of clipping. In RN 0.86.2:
Proposed fix (verified non-executing in-app; we could not build RN from source to device-test it):
Reproducer
https://github.com/muhasabahhub/android-react-native-arabic
Environment
| react-native | 0.86.2 (new architecture / Fabric,
newArchEnabled: true) || expo | ~57.0.15 |
| Font | Amiri Regular, bundled at
assets/fonts/Amiri-Regular.ttf(SIL OFL 1.1,assets/fonts/OFL.txt) || Reproduces on | Android 15 (API 35) and Android 16 (API 36) |
| Does not reproduce on | iOS |
The font matters: Amiri's alif-madda has ~2.2pt of ink past its advance box at
fontSize: 26. A low-overhang face such as Noto Naskh (~0.5pt) hides the bugwithout fixing it.
Screenshots
Steps to reproduce
rtl-ink-clip-repro
Minimal reproduction: Arabic leading ink is clipped at the start of an RTL line on Android.
Text 1 clips the alif-madda's leading ink at line start (Android 15+, RN 0.86/Fabric).
Text 2 renders the ink but flushes left.
iOS renders both correctly.
Repro steps
npm installnpx expo run:android— a dev build is required; this cannot be seen in Expo Go.<Text>'s own background,so the coloured rectangle is the view's box.
line of each paragraph. The console prints the first codepoint of every
rendered line via
onTextLayout, e.g.[text1-direction-rtl] line 2 starts U+0622— use it to confirm the word is positioned correctly.
FILLER_COUNT_BEFOREinApp.tsxand repeat.Both
<Text>elements are full-width with zero horizontal padding, deliberately:any side padding widens the view's bounds and masks the clipping.
Expected
The alif-madda's madda stroke renders in full, inside the tinted box, at the
right-hand (reading-start) edge of line 2 — as it does on iOS.
Actual
style={{ direction: 'rtl' }}): the paragraph is correctlyright-aligned, and the madda stroke is cut off vertically at the tint's
right edge. The ink that overhangs the glyph's advance box falls outside
the text view's bounds and is not drawn.
directionstyle, string prefixed with U+200F RLM): the sameglyph renders complete, because the paragraph is flushed to the left and
the leading edge is no longer at the view's boundary — so the ink has room.
This is not a usable workaround: the paragraph is now left-aligned.
Text 2 is included to show that the clipping is a function of where the line
starts relative to the view's bounds, not of the glyph or the font.
Likely cause
ReactAndroid/.../views/text/TextLayoutManager.ktbuilds every layout at asingle
StaticLayout.Builder.obtain(...)site and sets neither of the twoAndroid 15 APIs intended for exactly this case:
Notably, RN 0.86.2 already carries a lazily cached reflection handle for
setUseBoundsForWidth(TextLayoutManager.kt:108-118, with the comment"Reflection is needed because some internal targets compile against an SDK
older than 35") — but it has zero call sites, and no feature flag guards
it. The API is reached for and then never used.
ReactTextView.onDrawends insuper.onDraw(canvas), i.e. the view draws theLayoutthatTextView.makeNewLayout()builds for itself, so the equivalentTextViewsetters at attach time are a second possible site.Likely cause
ReactAndroid/.../views/text/TextLayoutManager.ktbuilds every layout at asingle
StaticLayout.Builder.obtain(...)site and sets neither of the twoAndroid 15 APIs intended for exactly this case:
Notably, RN 0.86.2 already carries a lazily cached reflection handle for
setUseBoundsForWidth(TextLayoutManager.kt:108-118, with the comment"Reflection is needed because some internal targets compile against an SDK
older than 35") — but it has zero call sites, and no feature flag guards
it. The API is reached for and then never used.
ReactTextView.onDrawends insuper.onDraw(canvas), i.e. the view draws theLayoutthatTextView.makeNewLayout()builds for itself, so the equivalentTextViewsetters at attach time are a second possible site.React Native Version
0.86.2
Affected Platforms
Runtime - Android
Output of
npx @react-native-community/cli infoStacktrace or Logs
MANDATORY Reproducer
https://github.com/muhasabahhub/android-react-native-arabic
Screenshots and Videos