Skip to content

Android: leading glyph ink clipped at RTL (Arabic) line starts - API 35 fix exists (setShiftDrawingOffsetForStartOverhang) but TextLayoutManager never sets it; existing reflection handle has zero call sites #58064

Description

@muhasabahhub

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

Image

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

  1. npm install
  2. npx expo run:android — a dev build is required; this cannot be seen in Expo Go.
  3. Look at the two tinted paragraphs. The tint is each <Text>'s own background,
    so the coloured rectangle is the view's box.
  4. 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.
  5. 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

Image

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions