Repository navigation
feat(text): add bold and color rich text tags - #729
Merged
Merged
Conversation
TextEcsComponent.text and shapeText parse <b>...</b> and <color=#rrggbb>...</color>. Tags are stripped before shaping, so kerning and wrapping see only the visible text; each GlyphQuad carries its tag color and a faux-bold embolden that the MSDF fill and effects shaders apply. Markup that isn't a valid tag renders literally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hwf91srGvueqSiuvLN5Kap
…t-tags # Conflicts: # documentation-site/src/pages/demos/text/index.tsx
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hwf91srGvueqSiuvLN5Kap
stormmuller
enabled auto-merge (squash)
October 6, 2026 21:59
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
10 tasks done
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.
Summary
Implements
design/rich-text-tags.md(backlog 5.7 ofdesign/ui-system.md), both phases.TextEcsComponent.textandshapeTextnow understand<b>...</b>and<color=#rrggbb>...</color>(#rgb,#rgbaand#rrggbbaaalso work).parseRichText(markup)returns the plain string plus flat, sorted, non-overlappingcolorRuns/boldRunsover its UTF-16 indices (DL-1, option b). Tags nest. When colors nest, the innermost wins.<b>and<color>are independent, so</b>closes the innermost open<b>even if a<color>was opened inside it.shapeTextparses the tags itself and then tokenizes, kerns and wraps the stripped string as before.shapeWordtracks each character's source index so it can stamp the style onto each glyph. A tagged string lays out exactly like its untagged equivalent.GlyphQuadhas two new fields:color?: Color:undefinedfalls back toTextEcsComponent.color, so recoloring untagged text still needs no reshape.embolden: number: the faux-bold edge shift in distance-field units,0for regular glyphs.<b>= synthetic bold (§7 option b). This is the same approach as FreeType'sFT_GlyphSlot_Embolden, browsers' fake bold and TextMeshPro's SDF dilate.FAUX_BOLD_EMBOLDEN(0.02 em) per side, and its advance grows by2 × 0.02 emso bold letters don't collide and wrap width accounts for them.shapeTextcall, from the atlas's pixels per em.distanceRange / 2pixels of padding.textEmboldenInstanceDataSegment(InstanceComponents.textEmbolden).msdf-fill.vert:sprite.vertplus the embolden passthrough.msdf-fill.fragandmsdf-effects.fragboth clamp the shift to the same atlas budget, so they agree on where the bold edge is.rendering-text.mdand a budget note intext-effects.md.[Unreleased].design/ui-system.mdrow 5.7 is marked landed and its non-goal line is reworded.design/rich-text-tags.mdis deleted, since everything in it is implemented.Changes from the design, given the current code
shapeTextparses tags itself. Task 1.2 had callers pass a pre-parsed string plus runs. Six docs demos callshapeTextdirectly to size guide boxes, so parsing inside means every measurement matches what's drawn, and no caller has to repeat the parse step.'middle'). That required one conflict fix in the demo blurb. The new e2e scene anchors by'baseline', so it doesn't depend on'middle'semantics.Decisions taken from the design's open questions
<b>mean? Option (b), faux bold from the existing atlas, which is the doc's recommendation. There's no new asset type and no change to the generator. The amount is a fixed engine constant, not an option, the same as browser and FreeType synthetic bold.</>/&. The doc's §8.2 proposal, the simpler rule: no escape syntax. A<that doesn't start a complete<name>,<name=value>or</name>is literal (HP < 50%,a<3). The exact text<b>can't be shown. This is documented in the guide. The solution reviewer recommended adding an escape (<noparse>or<) plus anescapeRichTexthelper for untrusted strings such as player names. I didn't add it, because the doc proposes no escaping for v1. It's a small follow-up if wanted.<not followed by a recognized tag name is literal. It also contradictsshapeText's existing rule that bad content "is a content problem, not a programming error". Throwing would also stop the game from a system that runs every frame on player or translated text. The solution reviewer flagged this as well. Chosen: an unknown tag (<i>), an invalid value (<color=red>,<b=1>) or a closing tag with nothing of its name open is drawn as literal text. An unclosed tag runs to the end of the string, as proposed.<color>carry alpha? As proposed, the run's color replaces RGB and alpha.#rgbaand#rrggbbaaset alpha, and#rgb/#rrggbbare opaque.opacityMultiplierstill applies on top.Solution reviewer verdict
REVISE, on four points:
It also recommended keeping the design doc. I deleted it anyway, because the coordinating task asks for implemented designs to be removed (as in #720). Otherwise it confirmed the approach: parsing inside
shapeText,GlyphQuad.coloroptional, one writer per value, no configurable queries, and faux bold matching TextMeshPro and FreeType.Related issue(s)
Backlog 5.7 in
design/ui-system.md.Verification checklist
npm run check-typespasses with 0 errors (alsocheck-types:e2e)npm testpasses (1993 tests; new:parse-rich-text.test.ts,text-embolden-instance-data-segment.test.ts, rich-text cases inshape-text.test.tsandglyph-quad.test.ts)npm run lintpasses with 0 errorsnpm run cspellpasses with 0 errorsnpm run check-exportspassesindex.ts/documentation-site/docs/docsis updatednpm run buildat the root, thentypecheckandbuildindocumentation-siterich-text-tags.spec.tschecks rendered pixels with relative, same-run measurements:<color>glyph renders green between red ones, at the same pixel positions as the untagged text.<b>glyph's ink is wider by2 × FAUX_BOLD_EMBOLDEN × size, and the glyphs after it shift right by the same amount.Changelog
## [Unreleased]inCHANGELOG.md🤖 Generated with Claude Code
https://claude.ai/code/session_01Hwf91srGvueqSiuvLN5Kap
Generated by Claude Code