3 Concepts
dxtr edited this page 2026-10-14 03:41:02 +02:00

Concepts

Faces, fonts and layout units

A face is a typeface: a face of a font file, with its variation settings. A font is a face at a size, which face-font gives; see Fonts. The size is in layout units per em. Every position a layout returns is a single-float in those units. What a layout unit means is up to you:

  • UI text: take the font at its pixel size, (face-font face 16), and a layout unit is a pixel.
  • Text in a scene: take the font at size 1 and a layout unit is an em; scale it into the world as you like.
  • Outlines and atlases: a font of size 1 gives outlines in ems, the usual unit for curve and distance-field textures.

Shaping runs at the font's own resolution, without hinting, and is scaled afterwards. A layout at size 32 is exactly a layout at size 16 scaled by two. Nothing is snapped to pixels; renderers that want pixel snapping do it themselves, as the raster backend does.

Text indexes

A text index is a position between two characters of the Lisp string: 0 is before the first character and (length text) after the last. Runs, lines, spans, carets and selections are all given in text indexes, and every glyph records the index of the character it came from.

Graphemes and caret stops

What a reader sees as one character can be several Lisp characters: e followed by a combining acute accent, a flag, or an emoji sequence joined by zero-width joiners. These are graphemes (extended grapheme clusters, UAX #29). cl-textrender never splits one:

  • the caret stops only at grapheme boundaries, the caret stops;
  • lines break inside a word only between graphemes;
  • font fallback picks one font for a whole grapheme.

Logical and visual order

Text is stored in logical order, the order it is read. It is displayed in visual order, left to right on the screen. The two differ for right-to-left scripts such as Hebrew and Arabic, and for numbers and Latin words inside them. cl-textrender resolves this with the Unicode Bidirectional Algorithm (UAX #9), through ICU:

  • Each paragraph has a base direction, :ltr or :rtl, from its first strong character unless you give one.
  • Each line is reordered on its own, after it is broken.
  • Runs on a line, and glyphs in a run, are in visual order: left to right in horizontal text, top to bottom in vertical text.

Hit testing works in both orders: caret-position and hit-test go between indexes and positions, and move-caret steps through the text in visual order, as arrow keys do.

What a layout is made of

layout-text returns a layout:

layout                 the text, its size, its lines
└── line ...           one line of text (a column, in vertical text)
    └── run ...        glyphs of one direction, script, font and style
        └── glyph ...  a glyph ID in a font, placed on the baseline
  • A layout has the string, the font it was given, its width and height, and its lines from top to bottom (right to left, for vertical text).
  • A line covers a range of the text, including trailing spaces and the newline that ends its paragraph. It knows its paragraph's direction, its baseline, its box (its share of the layout) and the part of that box its visible content takes up.
  • A run is a piece of a line with one direction, script, font and style, shaped together. Runs split wherever any of those change.
  • A glyph is what a renderer draws: the glyph ID, the font it is from, where its origin goes, its advance, and the :data its style carries.

All four are immutable structures. Lay the text out again when the text, fonts or width change.

Clusters

Shaping does not map characters to glyphs one to one. A ligature draws several characters with one glyph (ffi in "office"); a decomposed character may take several glyphs; Arabic letters change shape with their neighbours. A glyph's glyph-text-index is the index of the first character it was made from. Carets inside a ligature are spread evenly across it.

Paragraphs

Characters with the Unicode bidi class B end a paragraph: line feed, carriage return (with CR LF counted as one), U+2029 PARAGRAPH SEPARATOR and a few others. Each paragraph starts a new line, resolves its own direction, and is shaped on its own. The separator belongs to the end of its paragraph's last line and draws no glyph. A text that ends in a separator has a final empty line, where the caret goes after it.

Inline and block axes

Lines are laid along an inline axis and stacked along a block axis. In horizontal text the inline axis is x and lines stack downwards. In vertical text the inline axis is y, read top to bottom, and the lines are columns stacked right to left. Functions that return "the position along the line" mean x in horizontal text and y in vertical text.

Screen and world space

A layout is made in one of two y-axis conventions, which every y it returns and every y hit testing takes follow:

  • :y-axis :down, the default, is screen space: y grows downwards from the top of the first line.
  • :y-axis :up is world space: every y is negated, so the text hangs below the origin.

Coordinates and Units has the details, and how to place glyph outlines in either.

Styles and fallback

Every character has a style: a font fallback list, a list of OpenType features, and a piece of :data for the renderer. The font argument of layout-text gives the base style, and :spans override it for ranges of the text. Each grapheme is drawn from the first font of its style's list that has glyphs for all of it. See Styled Text and Fallback.

What cl-textrender does not do

  • Draw. Layout gives positions; renderers draw. The optional systems draw on the CPU or prepare textures, but upload nothing to a GPU.
  • Edit text. You own the string. Insert or delete characters yourself, then lay the text out again; the caret functions tell you where.
  • Find fonts. You give it font files or their bytes. There is no font discovery by family name.

Limitations lists what is not covered yet.