Repository navigation
Draw text runs as hollow outlines (text-outline) - #142
Merged
Merged
Conversation
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>
This was referenced Oct 20, 2026
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>
The old iOS outline stamped nudged copies of each glyph and cut the inside out. The cut removed the whole glyph, so what was left was a thin band outside the letter with rounded corners. It didn't look like Android's stroke. Now each outlined run's text is set again with Core Text in the same font, and the glyph paths are stroked where SwiftUI placed each glyph: centred on the edge, mitred corners, limit 4, as on Android and the web. Overlapping contours in a glyph (SF has them) are merged first so they don't show inside letters. Glyphs with no outline, like emoji, draw as they are. The font size is read from the laid-out run, so Dynamic Type still applies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
simonhamp
marked this pull request as ready for review
October 24, 2026 01:10
Android draws an outlined run's background, underline and strikethrough solid: its text layout switches the paint to fill for those even when the glyphs are stroked. iOS dropped all three, and drew stray bits of them at spaces, which have no outline to stroke. Outlined runs now lay out with see-through glyphs and decorations in the text colour. The renderer draws the run first, which leaves just its background, underline and strikethrough where SwiftUI puts them, then strokes the outlines on top. Emoji, which see-through text also hides, are drawn from their glyphs with Core Text. The leaf path gives its underline and strikethrough the text colour explicitly, because the first decoration modifier on a Text wins. 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 24, 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>
ricardovfreixo
added a commit
to ricardovfreixo/mobile-ui
that referenced
this pull request
Oct 25, 2026
Bounded merge for the companion PR NativePHP#162: takes upstream's bounded gesture areas (NativePHP#173), the text/font/keyboard/divider/sheet fixes (NativePHP#142, NativePHP#155, NativePHP#156, NativePHP#163, NativePHP#147, NativePHP#168, NativePHP#170) and the tab-row chrome fix (NativePHP#167), while keeping this branch's scroll-target seam intact — the live pager page availability guard (PagerPageContext), the ancestor pager chain in isAvailable(), and the Android list scroll-target registration. TabRowRenderer.kt resolves to upstream's version: both sides had independently removed the unsupported LocalTopBarColor / LocalTopTabRow hooks, so nothing was reintroduced and no stubs added.
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.
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-outlineclass and passes it as thetext_outlineprop. 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
Strokedraw style, at the width in dp. That's a real centred stroke with no fill.TextRenderersets each outlined run's text again with Core Text, in the same font, and strokes those glyph paths where SwiftUI placed each glyph. The stroke is centred on the edge with mitred corners (limit 4), the same as Android and the web.text-outline-0turns it off.The first version of the iOS side faked the stroke by stamping nudged copies of the glyph and cutting the inside out. The cut took out the whole glyph, so it drew a thin band outside the letter with rounded corners. The close-ups below show the difference.
Worth checking
underline/strikethroughexplicitly, because the first decoration modifier on aTextwins. That looks the same for text that isn't outlined.Textvalues joined together, because aTextRenderercan only see attributes set onText, not on anAttributedString. Each outlined run gets its own piece. 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.displayPaddingis twice the outline width, which is as far as a mitred corner can reach.Screenshots
iOS close-ups of the wordmark at 17pt (Geist, as in the app), before and after this change, light and dark, then the same at 40pt:
Android close-ups, light and dark:
Underline, strikethrough, a chip and an emoji in outlined runs on iOS, each next to the same run filled. Before this change iOS dropped all three, and left stray bits of them at spaces:
More iOS cases: the system font, a leaf, italics, Dynamic Type at accessibility sizes, and a run that wraps with an emoji in it. Then Android in the devnull app:
Testing
NativeUITextRenderer,NativeUIFontResolverandNativeUIThemewith stub nodes and the devnull app's Geist fonts. I ran it on iOS 27 (iPhone 17 Pro) and iOS 18.5 simulators. I haven't rebuilt the devnull app with this change yet.combined/dev-null-appbranches the devnull app uses.Docs: NativePHP/nativephp.com#565.
🤖 Generated with Claude Code