UI/Renderable: Text
Bitmap text renderable with size, font, colour, optional shader, and word-wrap/justify layout modes.
What this page covers #
text renders font-backed glyphs with runtime updates for content, size, spacing, alignment, colour, and automatic word-wrap/justify reflow.
By task #
- Define text options and defaults: see Definition and Notes
- Update string/font/spacing at runtime: see Runtime control
- Paint character ranges without markup: see Colour areas
- Enable word-wrap or justified text: see Text reflow and layout modes
- Align text correctly in slots and pivots: see Alignment and origin tips
Bitmap text with font, size, colour, optional shader override, and word-wrap/justify layout modes.
Import #
import * as UI from 'Helix/UserInterface';Definition #
type'text' - Renderable type identifier.stringstring optional - Text content to display.sizenumber optional - Character size in pixels.letterSpacingnumber optional - Spacing factor between letters (1.0= normal).lineSpacingnumber optional - Spacing factor between lines (1.0= normal).colour[r, g, b, a] optional - Text tint colour.fontstring optional - Font file path or Fontconfig name (e.g."/rom/fonts/SourceSans3-Regular.ttf"or"Inter"or"Inter:style=Bold").shader{ vert: string, frag: string } optional - Optional SPIR-V shader override.justify'left' | 'center' | 'right' optional - Horizontal glyph alignment within the text bounds.vAlign'start' | 'center' | 'end' optional - Vertical glyph alignment within the text bounds.layoutMode'simple' | 'word-wrap' | 'justify' optional - Text layout mode.'simple'(default) uses only explicit newlines.'word-wrap'breaks lines at word boundaries to fit a constraint width.'justify'is word-wrap plus inter-word spacing so left and right edges align. Requires a layout constraint (seelayoutBoundsorcontentAlign: 'fill').aabb{ min: { x, y }, max: { x, y } } optional - Bounding box that constrains reflow width for'word-wrap'and'justify'modes. Ignored in'simple'mode. If the element also hascontentAlign: 'fill', the fill bounds take priority.metricsBasis'line' | 'ink' optional - Bounds basis used bycontentAlignplacement.'ink'(default) uses tight glyph-ink bounds;'line'uses stable line-box metrics.
Notes #
- If a font is not specified at creation, you can set one later with
UI.setTextFont. - Changing
stringorsizerequires a re-prepare; the engine handles this inside setters. setTextString/setTextSize/setTextFontthrow if used on a non-text element.- Control text spacing at creation using
letterSpacingandlineSpacing, or change them at runtime withUI.setTextLetterSpacingandUI.setTextLineSpacing. UI.setFontOptions(windowId, fontKey, { mipmaps?, lightHinting? })configures rendering options for loaded fonts.fontKeyis the same string used when loading (path or Fontconfig name).- To align text within a layout slot, use
contentAlignon the element. To align glyphs within the text's own bounds, userenderable.justifyat creation orUI.setTextJustifyat runtime. - Vertical glyph alignment within the text bounds is controlled by
vAlignat creation orUI.setTextVerticalAlignat runtime. - Use
UI.setTextColourto change colour after creation. Alpha is optional; defaults to 1. - Colour areas override the base colour for selected character ranges without changing the base colour used by animations.
- Text uses its font atlas texture;
UI.setTexturehas no effect on text.
Colour areas #
A colour area paints a half-open character range [start, end) without embedding markup in the string. Indices count Unicode code points, not UTF-8 bytes, and include whitespace and newline characters. Areas remain attached to their numeric ranges when the string changes.
addColourAreareturns a stable numeric area ID.- Areas may overlap. The most recently added matching area takes precedence.
- Deleting the newest overlapping area reveals the earlier area beneath it; characters covered by no area use the base text colour.
- Base-colour animations continue to animate uncovered characters. Colour-area RGB and alpha remain fixed, while element/parent opacity still applies to every glyph.
- American-spelling aliases
addColorArea,listColorAreas, anddeleteColorAreaare also available.
const warning = await UI.addColourArea(win, textId, 6, 13, [1, 0.25, 0.15, 1]);
const emphasis = await UI.addColourArea(win, textId, 10, 17, [1, 0.85, 0.2, 1]); // wins on 10..12
const areas = await UI.listColourAreas(win, textId);
// [{ id, start, end, colour: [r, g, b, a] }, ...]
await UI.deleteColourArea(win, textId, emphasis); // warning is visible again on 10..12Text reflow and layout modes #
By default, Text uses 'simple' layout where only explicit \n characters create line breaks. Two additional modes add automatic word-wrapping and full justification.
Layout modes #
'simple'mode - Default. Only explicit\ncharacters produce line breaks. The text AABB is output-only (measured from glyph geometry).'word-wrap'mode - Lines break at word boundaries (spaces/tabs) when the line would exceed the constraint width. No hyphenation. Explicit\nstill produces hard breaks.'justify'mode - Same as'word-wrap', but inter-word spacing is expanded so the first and last word on each wrapped line align with the left and right edges of the constraint. Guardrails prevent excessive stretching.
Providing a constraint #
'word-wrap' and 'justify' modes require a width constraint to know where to break lines. Two sources can provide this:
contentAlign: 'fill'— the element's layout bounds (from its parent container) are used as the reflow constraint. This is the most common approach.aabb— an explicit AABB set at creation or viaUI.setTextAabbat runtime. Used whencontentFillis not active.
If both contentAlign: 'fill' and an explicit aabb are set, fill bounds take priority. This matches the precedence behavior of other renderables (Box, Circle).
Justification guardrails #
In 'justify' mode, the engine applies sane defaults to avoid ugly output:
- The last line of each paragraph (before a hard
\nor end of string) is not justified — it renders with normal spacing. - Lines with fewer than 3 words are not justified.
- If the extra space per word gap would exceed 1.5x the normal space width, justification is skipped for that line and it renders normally.
Overflow behavior #
When wrapped text exceeds the constraint height, all lines are still generated. Use clipToBounds: true on the element (or a parent) to visually clip overflow. The text renderer itself does not truncate.
Runtime control #
UI.setTextString(windowId, id, string)UI.setTextSize(windowId, id, px)UI.setTextColour(windowId, id, [r, g, b, a?])[start, end) and returns its stable ID. Newer overlapping areas take precedence.UI.addColourArea(windowId, id, start, end, colour): Promise<number>UI.listColourAreas(windowId, id): Promise<Array<{ id, start, end, colour }>>UI.deleteColourArea(windowId, id, areaId): Promise<boolean>UI.setTextFont(windowId, id, fontPath)UI.setTextJustify(windowId, id, 'left' | 'center' | 'right')UI.setTextVerticalAlign(windowId, id, 'start' | 'center' | 'end')UI.setTextLetterSpacing(windowId, id, factor)UI.setTextLineSpacing(windowId, id, factor)UI.setContentAlign(windowId, id, { x?, y? })UI.setFontOptions(windowId, fontPath, { mipmaps?, lightHinting? })UI.getRenderableBounds(windowId, id): { min, max, centre, size }UI.getRenderableSize(windowId, id): { x, y }UI.setTextLayoutMode(windowId, id, 'simple' | 'word-wrap' | 'justify')'word-wrap' and 'justify' modes.UI.setTextAabb(windowId, id, { min: { x, y }, max: { x, y } })contentFill is active, its bounds will still apply.UI.clearTextAabb(windowId, id)'line' for stable editable text placement and 'ink' for visually tighter static labels.UI.setTextMetricsBasis(windowId, id, 'line' | 'ink')lineCount reflects the number of rendered lines (including wrapped lines) rather than the input line count.UI.measureText(windowId, id): { size: { x, y }, lineAdvance, lineCount }UI.getTextCaretPosition(windowId, id, index): { x, y }UI.hitTestText(windowId, id, { x, y }): numberTop-level element options #
Pass these alongside renderable in UI.createElement (see also UI/Renderable: Shared properties):
UI.createElement(win, {
renderable: { type: 'text', /* ... */ },
contentAlign?: { x?: 'start'|'center'|'end', y?: 'start'|'center'|'end' } | 'fill'
})When contentAlign is 'fill', the renderable inherits the element's layout bounds. For text with layoutMode: 'word-wrap' or 'justify', this provides the reflow constraint automatically.
Example #
Creates a text element with custom font, spacing, and colour, then demonstrates updating every text property at runtime — string, size, spacing, font, justification, vertical alignment, colour, and font rendering options.
import * as UI from 'Helix/UserInterface';
const hello = await UI.createElement(win, {
renderable: {
type: 'text',
string: 'Hello',
size: 18,
letterSpacing: 1.1,
lineSpacing: 1.2,
colour: [1,1,1,1],
font: 'Inter:style=Medium',
position: { x: 8, y: 8 }
}
});
await UI.attachRoot(win, hello);
await UI.setTextString(win, hello, 'Hello, world!');
await UI.setTextSize(win, hello, 20);
await UI.setTextLetterSpacing(win, hello, 1.05);
await UI.setTextLineSpacing(win, hello, 1.25);
await UI.setTextFont(win, hello, 'Inter');
await UI.setTextJustify(win, hello, 'center');
await UI.setTextVerticalAlign(win, hello, 'center');
await UI.setTextColour(win, hello, [0.9, 0.9, 1.0, 1]);
await UI.setFontOptions(win, 'Inter', { mipmaps: false, lightHinting: true });Word wrap and justify example #
Creates a justified paragraph that reflows within a column layout.
import * as UI from 'Helix/UserInterface';
const column = await UI.createElement(win, {
layout: { type: 'column', gap: 8, padding: { l: 16, r: 16, t: 16, b: 16 } },
contentAlign: 'fill'
});
await UI.attachRoot(win, column);
const paragraph = await UI.createElement(win, {
renderable: {
type: 'text',
string: 'The quick brown fox jumps over the lazy dog near the river bank while evening light fades and distant bells ring softly.',
size: 18,
font: '/rom/fonts/SourceSans3-Regular.ttf',
colour: [1, 1, 1, 1],
layoutMode: 'justify'
},
contentAlign: 'fill'
});
await UI.attach(win, column, paragraph);
// Switch to plain word-wrap at runtime
await UI.setTextLayoutMode(win, paragraph, 'word-wrap');
// Or provide explicit bounds without fill
await UI.clearTextAabb(win, paragraph);
await UI.setTextAabb(win, paragraph, {
min: { x: 0, y: 0 },
max: { x: 400, y: 600 }
});Alignment and origin tips #
UI.setTextJustifyaligns glyph quads inside the text renderable's local bounds.- Combine justification with
renderable.originandrenderable.positionto anchor text precisely. If you're not using a parent layout, you can still usecontentAlignby giving the text asizeat creation (viaitem.size); alignment will occur within that explicit size.
To force bottom-middle origin after measuring:
const b = UI.getRenderableBounds(win, id);
UI.setOrigin(win, id, { x: b.centre.x, y: b.max.y });