v0.7.1 (build 925)

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 #

Bitmap text with font, size, colour, optional shader override, and word-wrap/justify layout modes.

Import #

import * as UI from 'Helix/UserInterface';

Definition #

TextRenderable
Bitmap text renderable with font, size, colour, and optional shader.
  • type 'text' - Renderable type identifier.
  • string string optional - Text content to display.
  • size number optional - Character size in pixels.
  • letterSpacing number optional - Spacing factor between letters (1.0 = normal).
  • lineSpacing number optional - Spacing factor between lines (1.0 = normal).
  • colour [r, g, b, a] optional - Text tint colour.
  • font string 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 (see layoutBounds or contentAlign: '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 has contentAlign: 'fill', the fill bounds take priority.
  • metricsBasis 'line' | 'ink' optional - Bounds basis used by contentAlign placement. '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 string or size requires a re-prepare; the engine handles this inside setters.
  • setTextString/setTextSize/setTextFont throw if used on a non-text element.
  • Control text spacing at creation using letterSpacing and lineSpacing, or change them at runtime with UI.setTextLetterSpacing and UI.setTextLineSpacing.
  • UI.setFontOptions(windowId, fontKey, { mipmaps?, lightHinting? }) configures rendering options for loaded fonts. fontKey is the same string used when loading (path or Fontconfig name).
  • To align text within a layout slot, use contentAlign on the element. To align glyphs within the text's own bounds, use renderable.justify at creation or UI.setTextJustify at runtime.
  • Vertical glyph alignment within the text bounds is controlled by vAlign at creation or UI.setTextVerticalAlign at runtime.
  • Use UI.setTextColour to 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.setTexture has 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.

  • addColourArea returns 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, and deleteColorArea are 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..12

Text 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 #

LayoutMode
Controls how text content is laid out within a bounding constraint.
  • 'simple' mode - Default. Only explicit \n characters 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 \n still 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 via UI.setTextAabb at runtime. Used when contentFill is not active.
Info

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 \n or 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
Update displayed text content.
UI.setTextString(windowId, id, string)
UI.setTextSize
Update character size in pixels.
UI.setTextSize(windowId, id, px)
UI.setTextColour
Update text tint colour. Alpha defaults to 1.
UI.setTextColour(windowId, id, [r, g, b, a?])
UI.addColourArea
Adds a colour override for Unicode code-point indices in [start, end) and returns its stable ID. Newer overlapping areas take precedence.
UI.addColourArea(windowId, id, start, end, colour): Promise<number>
UI.listColourAreas
Lists colour areas in insertion order.
UI.listColourAreas(windowId, id): Promise<Array<{ id, start, end, colour }>>
UI.deleteColourArea
Deletes a colour area. Returns false when the ID does not exist.
UI.deleteColourArea(windowId, id, areaId): Promise<boolean>
UI.setTextFont
Change the font by path or Fontconfig name.
UI.setTextFont(windowId, id, fontPath)
UI.setTextJustify
Set horizontal glyph alignment within text bounds.
UI.setTextJustify(windowId, id, 'left' | 'center' | 'right')
UI.setTextVerticalAlign
Set vertical glyph alignment within text bounds.
UI.setTextVerticalAlign(windowId, id, 'start' | 'center' | 'end')
UI.setTextLetterSpacing
Update letter spacing factor.
UI.setTextLetterSpacing(windowId, id, factor)
UI.setTextLineSpacing
Update line spacing factor.
UI.setTextLineSpacing(windowId, id, factor)
UI.setContentAlign
Align the renderable within its layout slot.
UI.setContentAlign(windowId, id, { x?, y? })
UI.setFontOptions
Configure rendering options for loaded fonts.
UI.setFontOptions(windowId, fontPath, { mipmaps?, lightHinting? })
UI.getRenderableBounds
Get local AABB of the text renderable.
UI.getRenderableBounds(windowId, id): { min, max, centre, size }
UI.getRenderableSize
Get text renderable width and height.
UI.getRenderableSize(windowId, id): { x, y }
UI.setTextLayoutMode
Change the text layout mode at runtime. Triggers a re-prepare.
UI.setTextLayoutMode(windowId, id, 'simple' | 'word-wrap' | 'justify')
UI.setTextAabb
Set the text bounding box. Constrains reflow width for 'word-wrap' and 'justify' modes.
UI.setTextAabb(windowId, id, { min: { x, y }, max: { x, y } })
UI.clearTextAabb
Remove the explicit text bounding box. If contentFill is active, its bounds will still apply.
UI.clearTextAabb(windowId, id)
UI.setTextMetricsBasis
Selects the bounds basis used by content alignment. Use 'line' for stable editable text placement and 'ink' for visually tighter static labels.
UI.setTextMetricsBasis(windowId, id, 'line' | 'ink')
UI.measureText
Get metrics for the text currently rendered by this text element. 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
Get local caret position for a codepoint index in the current text.
UI.getTextCaretPosition(windowId, id, index): { x, y }
UI.hitTestText
Map a local point to the nearest caret codepoint index.
UI.hitTestText(windowId, id, { x, y }): number

Top-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.setTextJustify aligns glyph quads inside the text renderable's local bounds.
  • Combine justification with renderable.origin and renderable.position to anchor text precisely. If you're not using a parent layout, you can still use contentAlign by giving the text a size at creation (via item.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 });