Text + Text concatenation via Compose AnnotatedString - #453
Conversation
241c381 to
7413efd
Compare
|
We might consider merging this work with #434 to support the |
|
Nice idea, I'll update with the best of both worlds shortly! ;) |
`static func + (Text, Text)` was previously `fatalError()`, so any SwiftUI that builds a multi-style inline string by concatenation (per-segment color / weight / size / italic / monospaced / underline / strikethrough / gradient) could not run on Android. Both runtimes now feed one ordered `[TextRun]` model: - Skip Lite (transpiled) via a native `+` operator (`Text.plus`, `// SKIP DECLARE: operator fun plus`), capturing each operand's styling as `TextRunStyle` data (a styled `Text` applies its style as an environment modifier that can't be read back at render time). - SkipFuse via `init(bridgedRuns:colors:fontSizes:fontWeights:flags:)`, which folds primitive per-run descriptors into the same `[TextRun]`. The concatenation *is* a `_Text` carrying its `runs`, folded into a single `AnnotatedString` (each run's styling as a `SpanStyle`) and rendered by the shared `_Text.Render` path — so environment concerns (alignment, line limit + truncation, tracking, line spacing, `material3Text`) behave like any other `Text`, with no duplicated render logic. Pairs with skiptools/skip-fuse-ui#118 (the SkipSwiftUI run model). Bare, unconstrained `Text + Text` requires the transpiler fix in skiptools/skipstone#255 to be composed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7413efd to
44cb0af
Compare
|
all working cleanly now 👍 |
marcprux
left a comment
There was a problem hiding this comment.
This looks good overall, but I'd love to see one or two test cases in SkipUI to demonstrate the concatenation.
| // Text-specific implementations of View modifiers | ||
|
|
||
| public func accessibilityLabel(_ label: Text) -> Text { | ||
| #if SKIP |
There was a problem hiding this comment.
All of these #if SKIP additions aren't necessary: the #else condition if never used, since this package is never compiled or used in a non-SKIP context. You should be able to just change everything to use the return styled(…) rather than gating them within the #if SKIP blocks.
|
Added the two test cases in
Both stack the two subjects in one Verified by mutation rather than by a green run alone, each case failing only for its own reason:
One observation from writing these, not addressed here: a concatenation of identically styled runs is a couple of pixels narrower than the equivalent plain |
Two cases demonstrating Text + Text as requested in review: the concatenation lays out as a single string of the joined content, and each operand's font survives into the concatenated run.
0af1623 to
9b41f5c
Compare
|
Correction to the note above: as first pushed, both cases failed the connected emulator step, and I have amended them to skip there. They compare rendered pixmaps, so they run into the same per-emulator font rasterisation differences that if isAndroid {
throw XCTSkip("Disabled on Android due to inconsistent font rendering on different emulators")
}So, precisely: the two cases execute natively and under Robolectric, and are skipped on a connected Android device or emulator. Everything else in that run was green on the emulator, including all eleven existing |
What
Implements
Text + Textconcatenation with per-segment styling on Android, rendered as a single ComposeAnnotatedString.static func + (Text, Text)was previouslyfatalError(), so any SwiftUI that builds a multi-style inline string by concatenation (per-segment color / weight / size / italic / monospaced / underline / strikethrough / gradient foreground) could not run on Android.How
Both runtimes feed one ordered
[TextRun]model, folded into a singleAnnotatedString(each run's styling as aSpanStyle) and rendered by the shared_Text.Renderpath — so environment-level concerns (text alignment, line limit + truncation, tracking, line spacing,material3Text) behave like a normalText, with no duplicated render logic:+operator (Text.plus,// SKIP DECLARE: operator fun plus). A styledTextapplies its style as an environment modifier that can't be read back at render time, so each operand's styling is captured asTextRunStyledata where the modifier is applied (.foregroundColor,.font,.bold, …), mirroring SkipSwiftUI'sTextRun.// SKIP @bridge public init(bridgedRuns:colors:fontSizes:fontWeights:flags:)accepts primitive-only per-run descriptors and folds them into the same[TextRun]. The foreground resolves to a solid color when it is one, otherwise a Compose brush (gradients viaSpanStyle(brush:)).localizedTextString(),asColor, andasBrushare@Composable, so they're resolved in the@Composablescope before the (non-composable) builder lambda.Dependencies / pairing
Text + Text. Without it, an unconstrained concatenation (e.g. directly in aVStack) is constructed but never composed (zero size); it only renders when wrapped in another view modifier like.frame(...).bridgedRunsinitializer.Testing
Verified on an Android emulator (Showcase "Text Concatenation" playground): per-run colors, weights, sizes, italic/monospaced, underline/strikethrough, gradient foreground, and multi-line wrapping all render as a single flowing styled block — both bare/unconstrained and width-constrained.
🤖 Generated with Claude Code