RUNE-306: Text Proposed
Summary
Rune exposes text shaping as a shared-resource API. rune_shape_text() returns an immutable ShapedText handle whose public query surface currently includes glyph count, total advance, per-glyph advances, ascent/descent metrics, UTF-16 text length, and logical-text-to-glyph cluster_map[]. Glyph IDs, glyph offsets, fallback font runs, caret-stop tables, and hit-test policy remain internal or framework-defined rather than part of the audited public API.
Motivation
Rune needs complex text support for mixed-direction text, ligatures, emoji, and fallback fonts without reintroducing a paragraph object that owns layout policy. The design keeps shaping in native code, keeps line layout in managed code, and preserves the snapshot command model where the renderer replays immutable resources rather than making layout decisions at draw time.
Design
Shaping API and Resource Model
RuneShapedText* rune_shape_text(RuneFont* font, float size, const void* text, uint32_t text_length);
uint32_t rune_shaped_text_glyph_count(RuneShapedText* shaped);
float rune_shaped_text_total_advance(RuneShapedText* shaped);
const float* rune_shaped_text_advances(RuneShapedText* shaped);
const uint32_t* rune_shaped_text_cluster_map(RuneShapedText* shaped);
uint32_t rune_shaped_text_text_length(RuneShapedText* shaped);
void rune_shaped_text_metrics(RuneShapedText* shaped, float* ascent, float* descent);
void rune_release(void* handle);ShapedText is a shared resource like Font, Image, and NineSlice: refcounted, immutable after creation, queried from C#, and released with rune_release() when the caller is done.
Internal Shaping Pipeline
rune_shape_text() currently performs the following native steps:
- initialize DirectWrite analysis source/sink over the caller text
- run BiDi analysis and script analysis
- map actual fallback fonts across the logical text range
- intersect font, script, and BiDi runs
- reorder those runs into visual order
- shape each run into internal glyph IDs, glyph offsets, advances, fallback font-run metadata, and the public
cluster_map[]
Glyph output is stored in visual order. cluster_map[] preserves the logical UTF-16 index to visual glyph-start mapping used by managed layout.
Managed Layout and Recording
The shipped Story text sprite performs line layout in C#:
- one paragraph is shaped at a time
- prefix sums are built from glyph advances plus optional letter spacing
- whitespace in the source string is mapped through
cluster_map[]to mark break opportunities - when no break opportunity fits, the current word is force-broken so the line cannot overflow forever
- each visual line is recorded as one
CmdDrawShapedTextglyph range with a baseliney
letterSpacing is post-shaping: layout and replay both apply the same extra gap between adjacent glyphs without mutating the shaped resource.
Cache Status
Rune exposes rune_set_shape_cache_limit(uint32_t bytes) / RuneEngine.SetShapeCacheLimit(uint bytes) as a process-wide budget knob, but the audited Direct2D shaping path does not currently populate a reusable shaped-segment cache. Each rune_shape_text() call shapes fresh input into a fresh ShapedText handle.
Interaction Surface
The current public text surface stops at advances, metrics, and cluster_map[]. There is no exported caret-stop array, glyph ID array, glyph-offset array, or standardized engine-level hit-test API in the audited implementation. Higher-level caret, selection, and hit-test policy remains framework/application responsibility.
Examples
Shape Once, Record Many
A label shapes one paragraph once, then records multiple CmdDrawShapedText glyph ranges from the same handle as wrapping changes.
Mixed Fallback Text
Shaping "Hello 你好 😀" starts from one base font handle, internally maps Latin, CJK, and emoji spans to different actual fonts, and still returns one public ShapedText resource.
Performance
- expensive script, bidi, and fallback analysis stays in the initial
rune_shape_text()call - one
CmdDrawShapedTextper visual line keeps replay cheaper than per-word draw commands - the current docs do not promise shaped-segment cache reuse because that cache is not implemented in the audited D2D path
Alternatives Considered
| Approach | Rejected because |
|---|---|
| native paragraph handle | would mix shaping, layout, trimming, and interaction into one render-owned special case |
| documenting public caret-stop and glyph-offset APIs | those exports do not exist in the audited implementation |
| claiming word-level shaping-cache reuse | the current D2D text path does not implement that cache |
Dependencies
- RUNE-304 defines
CmdDrawShapedText, the command recorded for each visual line. - RUNE-308 defines
ShapedTextas a shared resource with explicit lifetime viarune_release().
Test Strategy
- verify LTR text returns stable advances, metrics, and text length
- verify RTL and mixed BiDi text produce visual-order glyph storage with a correct
cluster_map[] - verify fallback shaping of mixed-script text uses the right actual fonts while remaining transparent to C# callers
- verify Story wrapping and line recording use advances plus
cluster_map[]without native paragraph layout - verify the public text surface does not claim caret-stop, glyph-ID, glyph-offset, or hit-test exports that are absent from the audited ABI