On the WASM/web build, ExecutorActionButton (and other EmojiText callers)
rendered mixed emoji+text labels wrong: the emoji glyph showed correctly
(e.g. the 🔍 magnifying glass) but the adjacent Latin text (“Source”,
“Target”, “Add Input Node”) rendered as broken squares (tofu). Desktop,
Android, and iOS were unaffected.
EmojiText (composeApp/…/EmojiSupport.kt) applied the WASM emoji font to the
entire text run: val effectiveFontFamily = EmojiSupport.emojiFontFamily ?: fontFamily,
then Text(fontFamily = effectiveFontFamily). On WASM emojiFontFamily is
NotoColorEmoji — an emoji-only font with no Latin glyphs — so every letter in
the string fell through to a missing glyph and rendered as tofu. Only the emoji
codepoints had glyphs, which is why the magnifying glass looked fine while the
word next to it did not. On other platforms emojiFontFamily is null, so the
whole-run override was a no-op and the bug was invisible there.
EmojiText now builds an AnnotatedString and applies SpanStyle(fontFamily =
emojiFontFamily) only to the emoji codepoint spans, computed by a new pure
helper emojiCharRanges(text). Latin text keeps the caller/default font (no
tofu); emoji still get NotoColorEmoji explicitly (no reliance on skiko
auto-fallback). When emojiFontFamily is null (desktop/Android/iOS) no spans
are added, so behavior on those platforms is byte-for-byte identical to before.
fontFamily of a text run that
can contain non-emoji characters — emoji fonts (NotoColorEmoji, etc.) carry no
Latin/ASCII glyphs, so the rest of the string tofus. Scope the emoji font to
emoji spans via AnnotatedString/SpanStyle instead.emojiCharRanges is unit-tested (EmojiCharRangesTest) with the invariant
that Latin text never lands in an emoji span — a deterministic guard that runs
on the desktop test JVM (no WASM/browser needed) and would have caught this.