Mobile Computing · UI Frameworks
Text Measurement and Line Breaking in Layout
How a layout engine turns a string, a font, and a width into glyph positions, why shaping cannot be done per character, and how the break opportunity rules interact with measurement.
A label is given a string, a font, and a maximum width. The layout engine must return a rectangle and a list of glyph positions. Between those two points sit three distinct operations, and treating them as one is the source of most text layout bugs: itemization, which splits the string into runs that share a script, a direction, and a font; shaping, which converts each run into positioned glyphs; and line breaking, which decides where the run may be divided to fit the width.
The order matters. Breaking cannot happen before shaping if the width of a candidate break is to be known, and shaping cannot happen per character because many scripts change glyph forms based on neighbours. A correct engine shapes first, then breaks, then may reshape the segments where a break changes the context.
Why per-character measurement is wrong
The naive model gives each character an advance width from the font’s horizontal metrics table and sums them. That is correct only for scripts where every character maps to exactly one glyph with a fixed advance. It fails immediately for Arabic, where a letter’s glyph depends on whether it joins its neighbours and in which direction; for Devanagari, where a consonant cluster is reordered and rendered as a single conjunct; for Latin ligatures such as the sequences that produce a single glyph for two characters; and for combining marks, which have zero advance and attach to a base glyph rather than occupying a position.
The OpenType layout tables express these rules as substitutions and positioning lookups applied in a defined order over a glyph sequence. Shaping is the process of applying them. HarfBuzz implements the OpenType shaping model and the Universal Shaping Engine for scripts without explicit OpenType coverage, and its interface is the de facto standard one: the caller provides a buffer of code points, a font, a script, a language, and a direction, and receives a buffer of glyph identifiers with advances and offsets.
The consequence for a UI framework is that a text node’s intrinsic width is not derivable from a per-character table, and that shaping a substring in isolation can produce a different result than shaping the whole string and taking a slice. A framework that caches glyph advances per character and then concatenates them will render Arabic joins incorrectly at run boundaries and produce wrong widths for any script with contextual forms.
The break opportunity rules
Line breaking is specified separately from shaping. Unicode Standard Annex #14 assigns each code point a line breaking class and defines a set of rules over pairs of classes that determine whether a break is permitted between them. The rules are ordered, and the first matching rule wins, which is why the algorithm is a state machine rather than a table lookup: the pair of classes is necessary but not sufficient, and rules such as the prohibition on breaking before a combining mark or between a base and its following modifier depend on more than two characters.
The classes include alphabetic, numeric, open and close punctuation, quotation marks, hyphens, spaces, and the ideographic classes for scripts that permit breaks between most characters. The result for a typical Latin paragraph is that breaks are permitted after spaces and after hyphens, prohibited before closing punctuation, and prohibited between a number and its unit when the appropriate no-break characters are present. For Chinese and Japanese, breaks are permitted between most ideographs with exceptions for characters that must not begin or end a line.
The algorithm does not know about widths. It produces a set of permitted break positions; the layout engine then chooses among them to satisfy the width constraint, typically greedily for left-aligned text. A greedy choice is optimal for the objective of minimising the number of lines, and it is not optimal for the objective of minimising raggedness, which is why typesetting systems use a dynamic program over the whole paragraph. Mobile layout engines overwhelmingly use the greedy algorithm, because a paragraph that reflows on every width change cannot afford an $O(n^2)$ pass over break candidates, and because the visual difference is small at the line lengths typical of a phone.
def greedy_breaks(widths, space_break_after, limit):
lines, start, last_break = [], 0, None
total = 0.0
for i, w in enumerate(widths):
total += w
if space_break_after[i]:
last_break = i + 1
if total > limit:
# Break at the last permitted opportunity; if none exists inside
# the line, the engine must break mid-word (overflow-wrap: anywhere).
cut = last_break if last_break is not None and last_break > start else i
lines.append((start, cut))
start, last_break, total = cut, None, sum(widths[cut:i + 1])
if start < len(widths):
lines.append((start, len(widths)))
return lines
Two behaviours in that sketch correspond to real specification decisions. When no permitted break exists within the limit, the engine either overflows the container or breaks at an arbitrary position, and which one happens is controlled by the overflow-wrap and word-break properties rather than by the line breaking algorithm. And the widths passed in are advances from shaping, so a break that splits a shaped run requires the engine to know whether reshaping the two halves changes their widths; for scripts with contextual forms it does, and the difference is not always zero.
Hyphenation and the measurement order
Automatic hyphenation adds break opportunities inside words, using a language-specific pattern set. The patterns produce positions where a hyphen may be inserted, subject to rules about the minimum number of characters on each side and about not leaving a fragment that is itself a word. Because a hyphen changes the rendered text, hyphenation cannot be applied after shaping without reshaping the fragment with the hyphen glyph inserted, and its advance differs from the sum of the two halves.
Frameworks expose hyphenation as a property that is off by default for most languages and on by default for some, and the default matters for measurement: a text node measured without hyphenation can be wider than the same node rendered with it, so a measurement API that does not take the property into account returns the wrong intrinsic size. This is one of the reasons intrinsic width queries are expensive: they must run the full pipeline, not just a metric lookup.
Font metrics and the baseline
Shaping produces advances, but layout needs more: the ascent, descent, and line gap from the font’s metrics determine the natural line height, and the baseline position within that height determines where the glyphs sit. Fonts disagree about what the ascent and descent should include. The OpenType hhea table and the OS/2 table’s typographic metrics can differ, and platform APIs choose between them differently, so the same font at the same size can produce a different line height on two platforms.
Most frameworks therefore define line height as a property of the text style rather than deriving it from the font, with the font metrics used only when the style does not specify. A specified line height larger than the font’s natural height distributes the extra space above and below the glyphs according to a distribution rule that differs between platforms, and the difference is visible when two lines of mixed fonts are stacked: one platform centres the glyphs in the line box, another places the extra space entirely below.
Vertical alignment of a single line inside a fixed-height container is the same problem in miniature. The distance from the top of the container to the baseline is the container height minus the line height, divided according to the alignment, plus the ascent. Rounding that sum to whole pixels at the device pixel ratio is what makes text sit one pixel higher on one screen than another, which is why text pipelines generally choose the baseline position in whole device pixels and let the glyph rasterizer handle subpixel placement relative to it.
Reflow, and what a width change really costs
A width change invalidates every line break decision for the paragraph and every shaped run whose segmentation depended on those breaks. The cost is not linear in characters but linear in the number of shaped runs plus the number of break candidates, with the constant dominated by shaping, which is the expensive part: a shaping call per run, each involving table lookups and possibly complex cluster analysis.
The standard mitigation is a layout cache keyed on the inputs that affect the result: the string, the style, the width, and the locale that determines hyphenation and line breaking rules. A list that reflows text on every row during a scroll cannot rely on such a cache, which is part of why virtualization, covered in View Recycling and Virtualization in Long Scrolling Lists, matters as much for text-heavy rows as for image-heavy ones. A width that changes continuously during a drag or an animation defeats such a cache, so frameworks typically round the width to a coarse granularity for cache purposes, or reflow only at the end of an animation, or animate the container’s transform rather than its width. The last is the cheapest and the most common: a scale transform on a text block changes its apparent width without changing the layout width at all, so no reflow occurs, at the cost of the glyphs being scaled rather than re-laid out, which looks wrong for text that should reflow.
The interaction with the layout protocol described in Constraints Down, Sizes Up: Why Layout Is a Single Pass is direct: a text node’s intrinsic width requires shaping the whole string, which is why intrinsic measurement of a text-heavy subtree is the most expensive operation a layout pass can perform, and why a container that asks for intrinsic sizes of many text children can turn a linear layout into something much worse.
References
- Unicode Standard Annex #14: Unicode Line Breaking Algorithm — Unicode Consortium
- HarfBuzz Documentation — HarfBuzz project
- CSS Text Module Level 3 — W3C