Skip to content

Text + Text concatenation via Compose AnnotatedString#453

Open
vincentborko wants to merge 1 commit into
skiptools:mainfrom
vincentborko:feat/text-concatenation
Open

Text + Text concatenation via Compose AnnotatedString#453
vincentborko wants to merge 1 commit into
skiptools:mainfrom
vincentborko:feat/text-concatenation

Conversation

@vincentborko

@vincentborko vincentborko commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

What

Implements Text + Text concatenation with per-segment styling on Android, rendered as a single Compose AnnotatedString.

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 foreground) could not run on Android.

How

Both runtimes feed one ordered [TextRun] model, folded into a single AnnotatedString (each run's styling as a SpanStyle) and rendered by the shared _Text.Render path — so environment-level concerns (text alignment, line limit + truncation, tracking, line spacing, material3Text) behave like a normal Text, with no duplicated render logic:

  • Skip Lite (transpiled) — a native + operator (Text.plus, // SKIP DECLARE: operator fun plus). A styled Text applies its style as an environment modifier that can't be read back at render time, so each operand's styling is captured as TextRunStyle data where the modifier is applied (.foregroundColor, .font, .bold, …), mirroring SkipSwiftUI's TextRun.
  • SkipFuse// 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 via SpanStyle(brush:)). localizedTextString(), asColor, and asBrush are @Composable, so they're resolved in the @Composable scope before the (non-composable) builder lambda.

Note (supersedes the original draft): the native Skip Lite + operator is implemented here now. Earlier this PR left + unimplemented and reached the run model only via the bridge; it has since been unified so Text + Text works in both Skip Lite and SkipFuse through the one _Text-with-runs path.

Dependencies / pairing

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.


  • AI was used to assist with this PR.

🤖 Generated with Claude Code

@marcprux

marcprux commented Jun 2, 2026

Copy link
Copy Markdown
Member

We might consider merging this work with #434 to support the + operator in Skip Lite as well as Fuse.

@vincentborko

Copy link
Copy Markdown
Contributor Author

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>
@vincentborko

Copy link
Copy Markdown
Contributor Author

all working cleanly now 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants