Rich Text Parsing & Rendering System
Texturge’s rich text system is a complete Parse → Build → Render pipeline that converts tagged rich text strings into a typed render tree capable of per-character animated rendering.
Pipeline Overview
RichText String FTagNode Tree FRenderNode Tree
┌──────────────────┐ ┌────────────────────┐ ┌──────────────────────────┐
│ "Hello <Wave │ │ Root │ │ Root (Unknown) │
│ id="bounce"> │ ───▶ │ ├─ "Hello " (text) │ ───▶ │ ├─ TextContent │
│ Texturge</>!" │ │ └─ Wave │ │ │ "Hello " │
│ │ │ └─ "Texturge" │ │ └─ AnimationLayer │
└──────────────────┘ └────────────────────┘ │ TagName="Wave" │
FTagParser Parse │ ├─ TextContent │
│ │ "Texturge" │
└──────────────────────────┘
FRenderTreeBuilder
Tag Parsing
FTagParser is a single-pass, non-recursive iterative stack parser that converts tagged rich text into a FTagNode parse tree and the stripped PlainText.
Supported Syntax
| Syntax | Example | Description |
|---|---|---|
| Opening tag | <tagname> | Tag names: [A-Za-z0-9_-], max 32 chars |
| Tag with attributes | <tagname attr1=val1 attr2="val2"> | Supports quoted and unquoted value formats |
| Auto-close | </> | Closes the most recently opened tag |
| Explicit close | </tagname> | Closes the named tag; warns on mismatch |
| Self-closing | <tag /> | Used for decorators (e.g., image placeholders) |
| Escape sequences | < > & | HTML/XML entity decoding |
Error Tolerance
- Malformed tags produce
UE_LOGwarnings, never crashes - Unclosed tags are auto-closed at end of input (each producing a warning)
- Invalid
<characters (not followed by a valid tag name) are treated as plain text
FTagNode Structure
struct FTagNode
{
FString TagName;
TMap<FString, FString> Attributes; // Key-value attributes
TArray<TSharedPtr<FTagNode>> Children; // Child nodes
TWeakPtr<FTagNode> Parent;
int32 RangeBegin; // Start index in plain text
int32 RangeEnd; // End index in plain text (exclusive)
bool bIsSelfClosing;
};
Render Tree Construction
FRenderTreeBuilder converts the parse tree into a typed render tree FRenderNode. Its core task is node classification — distinguishing animation tags, rich text style tags, and plain text.
Node Classification
| Type | Identification | Purpose |
|---|---|---|
AnimationLayer | Tag name matches an Entry in UTextAnimationDataAsset | Marks text segments needing independent animation |
StyleLayer | Tag name unmatched by DataAsset | Passed through to engine rich text (style tags defined in RichTextStyleSheet, e.g. <b>), preserved in output |
TextContent | Leaf node | Plain text content |
Decorator | Self-closing tag | Inline decorators (images, etc.) |
Entry Index Matching
FindEntryIndex() uses a two-tier matching strategy:
- Priority: Look up the
idattribute (e.g.,<anim id="Wave">), match byTagNameinAnimationData->Entries[] - Fallback: If no
idattribute, match directly by tag name (legacy<Wave>format compatible)
Display Text Serialization
SerializeToDisplayText() converts the render tree to UE5-compatible display rich text:
AnimationLayer→ skip tag markers, recurse into children (strip animation tags)StyleLayer→ output full tag wrapping (e.g.,<b>text</>)TextContent→ output text directlyDecorator→ output self-closing tag
SerializeSubstring() truncates the render tree by character index — the critical method for character-by-character reveal, dynamically generating properly style-wrapped partial text based on the current MaxCharIndex.
Slate Rendering
FTextRun — Per-Character Animation Rendering
FTextRun inherits FSlateTextRun and overrides OnPaint() for per-character animation rendering:
FTextRun::OnPaint()
│
├─ Animator present and playing?
│ ├─ Yes → For each character:
│ │ 1. Get FAnimationFrameData from Animator
│ │ 2. Apply transforms: Translate → Rotate → Scale → Shear (centered on Pivot)
│ │ 3. Apply color: Color × InWidgetStyle tint
│ │ 4. Apply opacity: Opacity × original opacity
│ │ 5. Draw shadow (if ShadowColor is non-transparent)
│ │ 6. Draw glyph
│ └─ No → Base FSlateTextRun::OnPaint() (fast path, zero overhead)
FRichTextMarshaller — Injecting Animated Text Runs
FRichTextMarshaller inherits FRichTextLayoutMarshaller, replacing standard FSlateTextRun with FTextRun in AppendRunsForText(), giving every text run a UTextAnimator reference.
The Marshaller maintains AccumulatedTextLength and PrecedingDecoratorCount to correctly compute each Run’s offset in PlainText — the key to mapping ModelText line indices back to PlainText global indices for frame data lookup.
SAnimatedRichTextBlock
SAnimatedRichTextBlock inherits SRichTextBlock, calling Animator->TickAnimation() in Tick(), and dynamically updating the widget text when the animator state changes. It uses FRichTextMarshaller to implement a unified pipeline for rich text parsing and animation data injection.
SAnimatedTextBlock
SAnimatedTextBlock inherits STextBlock, the Slate widget for the plain text path. Its OnPaint() reads frame data from Animator->GetCurrentFrameDataArray() and renders glyph by glyph. Unlike the RichText path, this widget doesn’t need Marshaller intervention — it directly iterates STextBlock’s text layout lines, looking up frame data for each glyph.