Text Rendering
What are the building blocks of a font?
Each glyph (i.e., character) is laid out on a baseline, with the portion above forming its ascent, and the portion below forming its descent. The glyph’s origin precedes the glyph and is anchored to the baseline. A cap line marks the upper boundary of capital letters; the mean line, or median, serves the same purpose for lowercase letters. Cap height and x-height are measured from the baseline to each of these lines, respectively. Ascenders extend above the cap line or the median, depending on capitalization. Descenders extend below the baseline. Tracking denotes letter spacing throughout a unit of text; kerning is similar, but only measured between adjacent glyphs. Height is given as the sum of ascent and descent. Leading denotes additional spacing split evenly above and below the line. Collectively, height and leading comprise the line height.
Font metrics are measured in logical units with respect to a box called the “em-square.”
Internally
, the em-square has fixed dimensions (e.g., 1,000 units per side). Externally, the em-square is scaled to the font size (e.g., 12 pixels per side). This establishes a correspondence between internal/logical units and external/physical units (e.g.,0.012
pixels per unit).Line height is determined by leading and the font’s height (ascent plus descent). If an explicit font height is provided, ascent and descent are constrained such that (1) their sum equals the desired height and (2) their ratio matches the original ratio. This alters the font’s spacing but not the size of its glyphs.
Note that explicitly setting the height to the font size is not the same as the default behavior.
When height isn’t fixed, the internal ascent and descent are used directly (after scaling). These values are font-specific and need not sum to the font size.
When height is fixed, ascent and descent have no relation to their internal counterparts (other than sharing a ratio). These values are chosen to sum to the target height (e.g., the font size).
Text size is further adjusted according to a text scale factor (an accessibility feature). This factor represents the number of logical pixels per font size pixel (e.g., a value of
1.5
would cause fonts to appear 50% larger). This effect is achieved my multiply the font size by the scale factor.A given paragraph of text may contain multiple runs. Each run (or text box) is associated with a specific font and may be broken up due to wrapping. Adjacent runs can be aligned in different ways, but are generally positioned to achieve a common baseline. Boxes with different line height can combine to create a larger line height depending on positioning. Overall height is generally measured from the top of the highest box to the bottom of the lowest.
What are the building blocks for describing text?
FontStyle
andFontWeight
characterize the glyphs used during rendering (specifying slant and glyph thickness, respectively).FontFeature
encodes a feature tag, a four-character tag associated with an integer value that customizes font-specific features (i.e. these can be anything from enabling slashed zeros to selecting random glyph variants).TextAlign
describes the horizontal alignment of text. “Left”, “right”, and “center” describe the text’s alignment with respect to its container. “Start” and “end” do the same, but in a directionality-aware manner. “Justify” stretches wrapped text such that it fills the available width.TextBaseline
identifies the horizontal lines used to vertically align glyphs (alphabetic or ideographic).TextDecoration
is a linear decoration (underline, overline, or a strikethrough) that can be applied to text; overlapping decorations merge intelligently.TextDecorationStyle
alters how decorations are rendered: using a solid, double, dotted, dashed, or wavy line.TextStyle
describes the size, position, and rendering of text in a way that can be transformed for use by the engine. This description includes colors, spacing, the desired font family, the text decoration, and so on.Specifying a fixed height generally alters the font’s default spacing. Ordinarily, the font’s ascent and descent (space above and below the baseline) is calculated without constraint. When a height is specified, both metrics must sum to the desired height while maintaining their original ratio; this is simply different than the default behavior.
TextPosition
represents an insertion point in rendered and unrendered text (i.e., a position between letters). This is encoded as an offset indicating the index of the letter immediately after the position, even if that index exceeds string bounds. An affinity is used to disambiguate the following cases where an offset becomes ambiguous after rendereding:Positions adjacent to automatic line breaks are ambiguous: the insertion point might be the end of the first line or the start of the second (with explicit line breaks, the newline is just another character, so there is no ambiguity).
Positions at the interface of right-to-left and left-to-right strings are ambiguous: the renderer will flip half the string, so it’s unclear whether the offset corresponds to the pre-render or post-render version (
e.x
., offset 3 can be “abc|ABC
” or “abcCBA
|”).
TextAffinity
resolves ambiguity when an offset can correspond to multiple positions after rendering (e.g., because a renderer might insert line breaks or flip portions due to directionality).TextAffinity.upstream
selects the option closer to the start of the string (e.g., the end of the line before a break, “abc|ABC
”) whereasTextAffinity.downstream
selects the option closer to the end (e.g., the start of the line after a break, “abcCBA
|”).TextBox
identifies a rectangular region containing text relative to the parent’s top left corner. Provides direction-aware accessors (i.e.,TextBox.start
,TextBox.end
).TextWidthBasis
enumerates approaches for measuring the width of a paragraph. Longest line selects the minimum space needed to contain the longest line (e.g., a chat bubble). Parent selects the width of the container for multi-line text or the actual width for a single line of text (e.g., a paragraph of text).RenderComparison
describes the renderable difference between two inline spans. Spans may have differences that will affect their layout (and painting), their painting, their metadata, etc.
What are the building blocks for describing paragraph layout?
ParagraphStyle
describes how lines are laid out byParagraphBuilder
. Among other things, this class allows a maximum number of lines to be set as well as the text’s directionality and ellipses behavior; crucially, it allows the paragraph’s strut to be configured.ParagraphConstraints
describe the input toParagraph
layout. Its only value is “width,” which specifies a maximum width for the paragraph. This maximum is enforced in two ways: (1) if possible, a soft line break (i.e., located between words) is inserted before the maximum is reached. Otherwise, (2) a hard line break (i.e., located within words) is inserted, instead.If this would result in an empty line (i.e., due to inadequate width), the next glyph is inserted irrespective of the constraint, followed by a hard line break.
This width is used when aligning text (via
TextAlign
); any ellipses is ignored for the purposes of alignment. Ellipses length is considered when determining line breaks [?].
StrutStyle
defines a “strut” which dictates a line of text’s minimum height. Glyphs assume the larger of their own dimensions and the strut’s dimensions (with ascent and descent considered separately). Conceptually, paragraph’s prepend a zero-width strut character to each line. The strut can be forced, causing line height to be determined solely by the strut.
What are the building blocks for placeholders in content?
Placeholders reserve rectangular spaces within paragraph layout. These spaces are subsequently painted using arbitrary content (e.g., a widget).
PlaceholderAlignment
expresses the vertical alignment of a placeholder relative to the font. Placeholders can have their top, bottom, or baseline aligned to the parent baseline. Placeholders may also be positioned relative to the font’s ascenders, descenders, or median.PlaceholderDimensions
describe the size and alignment of a placeholder. If a baseline-relative alignment is used, the type of baseline must be specified (e.g., alphabetic or ideographic). An optional baseline offset indicates the distance from the top of the box to its logical baseline (e.g., this is used to align inline widgets viaRenderBox.getDistanceToBaseline
).
What are the building blocks for inline spans of content?
InlineSpan
represents an immutable span of content within a paragraph. A span is associated with a text style which is inherited by child spans. Spans are added to aParagraphBuilder
viaInlineSpan.build
(this is handled automatically byTextPainter
); any accessibility text scaling is specified at this point. An ancestor span may be queried to locate the descendent span containing aTextPosition
.TextSpan
extendsInlineSpan
to represent an immutable span of styled text. The providedTextStyle
is inherited by all children, which may override all or some styles. Text spans contain text as well as any number of inline span children. Children form a text span tree that is traversed in order. Each node is also associated with a string of text (styled using the span’sTextStyle
) which effectively precedes any children.TextSpans
are interactive and have an associated gesture recognizer that is managed externally (e.g., byRenderParagraph
).PlaceholderSpan
extendsInlineSpan
to represent a reserved region within a paragraph.WidgetSpan
extendsPlaceholderSpan
to embed aWidget
within a paragraph. Widgets are constrained to the maximum width of the paragraph. The widget is laid out and painted byRenderParagraph
;TextPainter
simply leaves space within the paragraph.ParagraphBuilder
tracks the number of placeholders added so far; this number is used when building to identify thePlaceholderDimensions
corresponding to this span. These dimensions are typically computed byRenderParagraph
, which lays out all widget spans before building the paragraph so that the necessary amount of space is reserved.
What are the building blocks for building paragraphs?
Paragraph is a piece of text wherein each glyph is sized and positioned appropriately;
Paragraph.layout
must be invoked to compute these metrics. Paragraph supports efficient resizing and painting (viaCanvas.addParagraph
) and must be built byParagraphBuilder
. Each glyph is assigned an integer offset computed before rendering (thus there is no affinity issue). Note thatParagraph
is a thin wrapper around engine code.After layout, paragraphs can report height, width, longest line width, and intrinsic width. Maximum intrinsic width maximally reduces the paragraph’s height. Minimum intrinsic width is the smallest width allowing correct rendering.
Paragraphs can report all placeholder locations and dimensions (reported as
TextBox
instances), theTextPosition
associated with a 2D offset, and word boundaries given an integer offset.Once laid out,
Paragraphs
can provide bounding boxes for a range within the pre-rendered text. Boxes are derived from runs of text, each of which may have a distinct style and therefore height. Thus, boxes can be sized in a variety of ways (viaBoxHeightStyle
,BoxWidthStyle
). Boxes can tightly enclose only the rendered glyphs (the default), expand to include different portions of the line height, be sized to the maximum height in a given line, etc.
ParagraphBuilder
assembles a singleParagraph
from a sequence of text and placeholders using the providedParagraphStyle
.TextStyles
are pushed and popped, allowing styles to be inherited and overridden, and placeholders boxes (e.g., for embedded widgets) are reserved and tracked. TheParagraph
itself is built by the engine.
What are the text rendering building blocks?
TextOverflow
describes how visual overflow is handled when rendering text viaRenderParagraph
. Options include clipping, fading, adding ellipses, or tolerating overflow.TextPainter
performs the actual work of building aParagraph
from anInlineSpan
tree and painting it to the canvas; the caller must explicitly request layout and painting. The painter incorporates the various text renderingAPIs
to provide a convenient interface for rendering text. If the container’s width changes, layout and painting must be repeated.Text layout selects a width that is within the provided minimum and maximum values, but that is as close to the maximum intrinsic width as possible (i.e., consumes the least height). Redundant calls are ignored.
TextPainter
supports a variety of queries; it can provide bounding boxes for aTextSelection
, the height and offset of the glyph at a givenTextPosition
, neighboring editable offsets, and can convert a 2D offset into aTextPosition
.A preferred line height can be computed by rendering a test glyph and measuring the resulting height (via
TextPainter.preferredLineHeight
).
Text is a wrapper widget for
RichText
which obtains the ambient text style viaDefaultTextStyle
.RichText
is aMultiChildRenderObjectWidget
that configures a singleRenderParagraph
with a providedInlineSpan
. Its children are obtained by traversing this span to locateWidgetSpan
instances. Any render objects produced by the associated widgets will be attached to theRichText
’sRenderParagraph
; this is how theRenderParagraph
is able to layout and paint widgets into the paragraphs’ placeholders.TextParentData
is the parent data associated with the inline widgets contained in a paragraph of text. It extendsContainerBoxParentData
to track the text scale applied to the child.RenderParagraph
displays a paragraph of text, optionally containing inline widgets. It delegates to (and augments) an underlyingTextPainter
which builds, lays out, and paints the actual paragraph. Any operations that depend on text layout generally recompute layout blindly; this is acceptable sinceTextPainter
ignores redundant calls.
How is text laid out by the render tree?
RenderParagraph
adapts aTextPainter
to the render tree, adding the ability to render widgets (i.e.,WidgetSpan
instances) within the text. Any such widgets are parented to theRenderParagraph
; a list of all descendent placeholders (RenderParagraph._placeholderSpans
) is also cached. TheRenderParagraph
proxies the various inputs to the text rendering system, invalidating painting and layout as values change (RenderComparison
allows this to be done efficiently).Layout is performed in three stages: inline children are laid out to determine placeholder dimensions, text is laid out with placeholder dimensions defined, and children are positioned using the final placeholder positions computed by the engine. Finally, the desired
TextOverflow
effect is applied (by clipping, configuring an overflow shader, etc).Children (i.e., inline widgets) are laid out in sequence with infinite height and a maximum width matching that of the paragraph. A list of
PlaceholderDimensions
is assembled using the resulting layout. If the associatedInlineSpan
is aligned to the baseline, the offset to the child’s baseline is computed (viaRenderBox.getDistanceToBaseline
); this ensures that the child’s baseline is coincident with the text’s baseline. Finally, the dimensions are bound to theTextPainter
(viaTextPainter.setPlaceholderDimensions
).Next, the box constraints are converted to
ParagraphConstraints
(i.e., height is ignored). If wrapping or truncation is enabled, the maximum width is retained; otherwise, the width is considered infinite. These constraints are provided toTextPainter.layout
, which builds and lays out the underlyingParagraph
(via the engine).Finally, children are positioned based on the final text layout. Positions are read from
TextPainter.inlinePlaceholderBoxes
, which exposes an ordered list ofTextBox
instances.
How is text painted by the render tree?
Since
TextPainter
is stateful, and certain operations (e.g., computing intrinsic dimensions) destroy this state, the text must be relaid out before painting.Any applicable clip is applied to the canvas; if the
TextOverflow
setting requires a shader, the canvas is configured accordingly.The
TextPainter
paints the text (viaCanvas.drawParagraph
). Empty spaces will appear in the text wherever placeholders were included.Finally, each child is rendered at the offset corresponding to the associated placeholder in the text. If applicable, the text scale factor is applied to the child during painting (e.g., causing it to appear larger).
How is text made interactive?
All
InlineSpan
subclasses can be associated with a gesture recognizer (InlineSpan.recognizer
). This instance’s lifecycle, however, must be maintained by client code (i.e., theRenderParagraph
). Additionally, events must be propagated to eachInlineSpan
since spans do not support bubbling themselves.The
RenderParagraph
overridesRenderObject.handleEvent
to attach new pointers (i.e.,PointerDownEvent
) directly to the span that was tapped.The affected span is identified by first laying out the text, mapping the event’s offset to a
TextPosition
, then finally locating the span that contains this position (viaTextPainter.getPositionForOffset
andInlineSpan.getSpanForPosition
, respectively).
The
RenderParagraph
is also responsible for hit testing any render objects included via inline widgets. These are hit tested using a similar approach asRenderBox
that additionally takes into account any text scaling.The
RenderParagraph
is always included in the hit test result.
How are text’s intrinsic dimensions calculated?
Intrinsic dimensions cannot be calculated if placeholders are to be aligned relative to the text’s baseline; this would require a full layout pass according to the
RenderBox
layout protocol.Intrinsics are dependant on two things: inline widgets (children) and text layout. Children must be measured to establish the size of any placeholders in the text. Next, the text is laid out using the resulting placeholder dimensions so its that intrinsic dimensions can be retrieved (
Paragraph.layout
is required to obtain the intrinsic dimensions from the engine).The minimum and maximum intrinsic width of text is computed without constraint on width. The minimum value corresponds to the width of the first word and the maximum value corresponds to the full string laid out on a single line.
The minimum and maximum intrinsic height of text is computed using the provided width (text layout is width-in-height-out). Moreover, the maximum and minimum values are equivalent: the text’s height is solely determined by its width. Thus, both values match the height of the text when rendered within the given width.
Intrinsic dimensions for all children are queried via the
RenderBox
protocol. The input dimension (e.g., height) is provided to obtain the opposite intrinsic dimension (e.g., minimum intrinsic width); this value is used to obtain the remaining intrinsic dimension (e.g., minimum intrinsic height). These values are then wrapped in aPlaceholderDimension
instance which is aligned appropriately (viaRenderParagraph._placeholderSpans
).Variants obtaining minimum and maximum intrinsic dimensions are equivalent other than the render box methods they invoke.
Both the intrinsic width and height children must be computed since the placeholder boxes affect both dimensions of text layout. However, when measuring maximum intrinsic width, height can be ignored since text is laid out in a single line.
The intrinsic sizes of all children are provided to the text painter prior to layout (via
TextPainter.setPlaceHolderDimensions
). This ensures that the resulting intrinsics accurately reflect any inline widgets. After layout, intrinsics can be read from theParagraph
directly. Note that this is destructive in that it wipes out earlier dimensions associated with theTextPainter
.
How is text directionality handled by the framework?
Unlike other frameworks,
Flutter
does not have a default text direction (TextDirection
). Throughout the lower levels of the framework, directionality must be specified. At the widget level, an ambient directionality is introduced by theDirectionality
widget. Other widgets use the ambient directionality when interacting with lower level aspects of the framework.Often, a visual and a directional variant of a widget is provided (
EdgeInsets
vs.EdgeInsetsDirectional
). The former exposes top, left, right, and bottom properties; the latter exposes top, start, end, and bottom properties.Painting is a notable exception; the canvas (Canvas) is always visually oriented, with the X and Y axes running from left-to-right and top-to-bottom, respectively.
How does the engine layout text?
Minikin measures and lays out the text.
Minikin uses
ICU
to split text into lines.Minikin uses
HarfBuzz
to retrieve glyphs from a font.
Skia paints the text and text decorations on a canvas.
Last updated