Skip to content

Draw text runs as hollow outlines (text-outline) - #142

Draft
simonhamp wants to merge 1 commit into
mainfrom
feat/text-outline
Draft

simonhamp wants to merge 1 commit into
mainfrom
feat/text-outline

Conversation

@simonhamp

@simonhamp simonhamp commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Text runs can now draw hollow: just the outline of their glyphs, in the run's own colour. This goes with NativePHP/mobile-air#549, which adds the text-outline class and passes it as the text_outline prop. I needed it for the dev/null wordmark, whose middle slash the web draws as an orange outline with -webkit-text-stroke.

How it draws

  • Android: the span or leaf gets Compose's Stroke draw style, at the width in dp. That's a real centred stroke with no fill.
  • iOS: SwiftUI can't stroke glyphs, so on iOS 18 a TextRenderer builds the outline out of the glyphs themselves. Copies nudged out by half the width in 16 directions make the outer edge. The part that copies nudged in by half the width all still cover is cut out of that, which makes the inner edge. The result is the same centred stroke.
  • Before iOS 18, outlined runs draw filled as before.
  • Nested runs inherit the width, like the other text props, and text-outline-0 turns it off.

Worth checking

  • The iOS outline's corners come out slightly rounded, because the copies are nudged around a circle. Android's are sharp miters, like the web's. At 1pt you have to zoom in to see it.
  • A composed text with an outlined run is built as several Text values joined together, because a TextRenderer can only see attributes set on Text, not on an AttributedString. Composed text without an outline still builds the one attributed string it always did, and the custom renderer is only installed when something is outlined.
  • The renderer's displayPadding is half the outline width, so the outline isn't clipped at the edges.

Screenshots

From the devnull app, where the wordmark's middle slash uses text-outline text-theme-lane-0. The close-ups show the web on top, the app's earlier solid slash in the middle, and text-outline at the bottom, all at 17pt on iOS:

Light Dark

Android close-ups, light and dark:

iOS light iOS dark Android light Android dark

Testing

  • iOS: built and run on an iPhone 17 Pro simulator (iOS 27). I also typechecked the plugin's Swift with mobile-air's render sources: same error count as main, all from types outside those folders, plus a planted error to prove the file was checked.
  • Android: built (BUILD SUCCESSFUL) and run on an API 35 emulator.
  • For the test builds, both changes were applied on top of the combined/dev-null-app branches the devnull app uses.

Docs: NativePHP/nativephp.com#565.

🤖 Generated with Claude Code

A text run with text_outline draws hollow: the outline of its glyphs, that
many points wide and centred on their edges, in the run's colour. It works
on leaf text and on runs inside composed text, and nested runs inherit it.

Android draws it with Compose's Stroke draw style. SwiftUI can't stroke
glyphs, so on iOS 18 a TextRenderer builds the outline from the glyphs:
copies nudged out by half the width make the outer edge, and the overlap
of copies nudged in, cut out of it, makes the inner edge. Text without an
outline keeps the single attributed string it always had. Before iOS 18
outlined runs draw filled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
simonhamp added a commit that referenced this pull request Oct 20, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
simonhamp added a commit to NativePHP/dev-null-app that referenced this pull request Oct 20, 2026
The /dev/null wordmark's middle slash is now a hollow lane-0 outline, like
the web's, using text-outline from NativePHP/mobile-air#549 and
NativePHP/mobile-ui#142. It replaces the solid regular-weight slash #20
used because EDGE text couldn't stroke a glyph then.

Main's lock already has a mobile-air combined head with #549. This moves
nativephp/mobile-ui to the combined/dev-null-app head that has #142.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
simonhamp added a commit that referenced this pull request Oct 22, 2026
Brings in #149 (with #151), #152 and the final #145, plus everything else
merged to main since #144. The four conflicts were all in files where
combined's own changes (#145, #149, custom icons, the pager header and the
sticky header) are now on main in their final form, so main's version wins.
Combined still carries text-outline (#142) and the Android sheet handle on
top of main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant