This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Laying Out Text
(tr:layout-text text font
&key width (wrap t) (align :start) (direction :auto)
(line-spacing 1.0) (y-axis :down) spans
(orientation :horizontal) tab-stops max-lines
(ellipsis "…"))
layout-text lays out a string and returns a layout. font is a font,
or a list of fonts to fall back on. The keyword arguments:
| Argument | Values | Default | Meaning |
|---|---|---|---|
:width |
positive real or nil |
nil |
The longest a line may be, in layout units (the column height, in vertical text). nil keeps each paragraph on one line, as long as it is. |
:wrap |
generalized boolean | t |
Whether lines wrap at :width. With nil, each paragraph stays on one line, cut short where it is longer than :width; see Typography. |
:align |
:start :end :left :right :center :justify |
:start |
Where each line sits within the width. :start and :end follow each paragraph's direction. |
:direction |
:auto :ltr :rtl |
:auto |
The base direction. :auto lets each paragraph's first strong character decide. |
:line-spacing |
positive real | 1.0 |
A multiplier for the line height. |
:y-axis |
:down :up |
:down |
Screen space or world space; see Coordinates and Units. |
:spans |
list | () |
Styles for ranges of the text; see Styled Text and Fallback. |
:orientation |
:horizontal :vertical |
:horizontal |
Lines or columns; see Vertical Text. |
:tab-stops |
positive real, list of increasing ones, or nil |
nil |
Where tabs go; see Typography. |
:max-lines |
positive integer or nil |
nil |
The most lines to keep; see Typography. |
:ellipsis |
string or nil |
"…" |
What ends the last line when text is left out, and a line cut short. |
A value outside these choices signals a type-error.
In the examples, font is DejaVu Sans at size 16, as in the first one.
A first layout
(tr:with-font (font "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
:size 16)
(let ((layout (tr:layout-text "The quick brown fox" font :width 80)))
(map 'list (lambda (line)
(list (subseq (tr:layout-string layout)
(tr:line-start line)
(tr:line-end line))
(tr:line-baseline line)
(tr:rect-width (tr:line-content line))))
(tr:layout-lines layout))))
;; => (("The quick " 14.8515625 77.64844)
;; ("brown fox" 33.476563 78.88281))
The first line holds "The quick " with its trailing space; its content,
not counting that space, is 77.65 wide. The layout is 80 wide, the
:width, and 37.25 high, two lines of 18.625, so each line's box is 80 by
18.625.
Reading the result
The result is a tree of immutable structures (see Concepts):
layout-linesis a vector oflines, from the top.line-runsis a vector ofruns, in visual order along the line.run-glyphsis a vector ofglyphs, in visual order along the line.
Most renderers only need the glyphs. do-glyphs walks them all, line by
line, without allocating:
(tr:do-glyphs (glyph layout)
(draw-glyph (tr:glyph-font glyph) (tr:glyph-id glyph)
(tr:glyph-x glyph) (tr:glyph-y glyph)
(tr:glyph-data glyph))) ; DRAW-GLYPH is yours
do-glyphs takes an optional result form, (do-glyphs (g layout result) ...), and return leaves it early. layout-glyphs returns the same glyphs
in a fresh vector.
Layouts
| Accessor | What it is |
|---|---|
layout-string |
The text that was laid out. |
layout-font |
The font or font list layout-text was given. |
layout-lines |
A vector of lines, in order. |
layout-width, layout-height |
The layout's size in layout units. |
layout-y-axis |
:down or :up. |
layout-orientation |
:horizontal or :vertical. |
layout-truncated-p |
True if text was left out: lines past :max-lines, or what lines cut short with :wrap nil cut off. |
The width is :width when one was given, and otherwise the widest line.
The height is the sum of the line heights. (In vertical text the two swap:
see Vertical Text.)
Lines
| Accessor | What it is |
|---|---|
line-start, line-end |
The range of text the line covers, including trailing whitespace and, on a paragraph's last line, the newline. |
line-direction |
Its paragraph's direction, :ltr or :rtl. |
line-runs |
A vector of runs, in visual order. |
line-baseline |
The baseline's y. |
line-box |
A rect: the line's share of the layout, the layout's width by the line's height. |
line-content |
A rect: the part of the line box the visible content takes up, from the first glyph to the last, trailing whitespace left out. |
line-ascent, line-descent |
The largest ascent and descent of the fonts on the line. |
line-truncated-p |
True for a line cut short to fit an ellipsis. Such a line covers only the text it shows. |
Runs
| Accessor | What it is |
|---|---|
run-start, run-end |
The range of text the run covers. Equal for an ellipsis, which covers no text. |
run-direction |
:ltr, :rtl, or :ttb for upright vertical text. |
run-script |
The ISO 15924 script code, such as "Latn", "Hebr" or "Hani", or nil for an ellipsis. |
run-font |
The font the run's glyphs come from. |
run-data |
The :data of the run's style. |
run-glyphs |
A vector of glyphs, in visual order along the line. |
run-box |
A rect: the part of the line box the run takes up, trailing whitespace included. |
Glyphs
| Accessor | What it is |
|---|---|
glyph-id |
The glyph index in glyph-font. |
glyph-font |
The font the glyph comes from: a fallback font, if the style's first font lacked it. |
glyph-text-index |
The index of the first character the glyph was made from. |
glyph-x, glyph-y |
Where the glyph's origin on the baseline goes, in layout units. |
glyph-advance |
How far the glyph advances along the line, after shaping, tabs and justification. |
glyph-data |
The :data of the glyph's style, such as a color. |
glyph-rotated-p |
True for a glyph turned 90° clockwise in vertical text. |
Paragraphs
Newlines split the text into paragraphs, each starting a new line. A text ending in a newline gets an empty last line, and an empty text has one empty line:
(defun lines-of (layout)
"Return the text of each line of LAYOUT."
(map 'list (lambda (line)
(subseq (tr:layout-string layout)
(tr:line-start line) (tr:line-end line)))
(tr:layout-lines layout)))
(lines-of (tr:layout-text (format nil "one~%~%three~%") font))
;; => ("one
;; " "
;; " "three
;; " "")
Line feed, carriage return, CR LF, U+2029 and the other characters of bidi class B all end a paragraph. They draw no glyph.
Wrapping
With :width, each paragraph is broken into lines no longer than the
width:
- Break opportunities come from ICU's line breaking (UAX #14): after spaces, after hyphens, between CJK ideographs, and so on. Lines are filled greedily: each takes as much text as fits.
- Trailing whitespace hangs. Spaces at the end of a line may go past the
width. They belong to the line, but
line-contentleaves them out. - Long words break. A word wider than the line breaks between graphemes, at the last boundary that fits.
- Nothing breaks a grapheme. A single grapheme wider than the line gets a line of its own.
- Tabs count at their expanded width.
Without :width, paragraphs are not wrapped. With :wrap nil, they are
not wrapped either, but a line longer than :width is cut short: see
Typography.
Alignment
:align places each line's content within the width:
:align |
Left-to-right paragraph | Right-to-left paragraph |
|---|---|---|
:start |
left | right |
:end |
right | left |
:left |
left | left |
:right |
right | right |
:center |
centered | centered |
:justify |
filled edge to edge, last line at the start | filled edge to edge, last line at the start |
(tr:layout-text "The quick brown fox" font :width 80 :align :center)
;; rect-x of the two lines' line-content => 1.1757813 and 0.55859375
Without :width, lines align within the widest line. Trailing spaces hang
past the line's end edge: the right in left-to-right text, the left in
right-to-left text. See Typography for
:justify.
Direction
Each paragraph has a base direction. With :direction :auto, the first
strong character decides: a paragraph starting with Hebrew or Arabic is
right to left, so :start aligns it to the right and its runs read from the
right:
(tr:layout-text "שלום world" font :width 120)
;; one line: direction :RTL, its line-content from x 35.45 to 120.0
;; runs, left to right: "world" (5..10, :LTR) then "שלום " (0..5, :RTL)
:direction :ltr or :rtl sets the base direction of every paragraph. It
changes where neutral characters such as spaces and punctuation go, and
the default alignment; strong characters keep their own direction.
Mixed text is reordered per line. In a left-to-right paragraph, a Hebrew word becomes a right-to-left run in the middle:
(map 'list (lambda (run)
(list (tr:run-start run) (tr:run-end run)
(tr:run-direction run) (tr:run-script run)))
(tr:line-runs
(aref (tr:layout-lines (tr:layout-text "Hello שלום world" font)) 0)))
;; => ((0 6 :LTR "Latn") (6 10 :RTL "Hebr") (10 11 :LTR "Hebr") (11 16 :LTR "Latn"))
The space after the Hebrew word is a run of its own: it is in the Hebrew script run but resolves to left to right.
Line height and spacing
Each line is as tall as the fonts on it need: its height is
line-spacing × (largest ascent + largest descent + largest line gap)
over the fonts its glyphs come from (or the font at its start, for an empty
line). The space beyond ascent and descent, from the line gap and from
:line-spacing, is split equally above and below the text. The baseline is
at the line's top plus half that space plus the ascent:
(tr:layout-text (format nil "a~%b") font :line-spacing 1.5)
;; line 0: top 0.0, baseline 19.507813, bottom 27.9375
;; line 1: top 27.9375, baseline 47.445313, bottom 55.875
line-ascent and line-descent give the values a line used. Lines with a
larger font in a span grow to fit it; see
Styled Text and Fallback.
When to lay out again
Lay the text out again when the text, the fonts, the width or any option changes; keep the layout and draw from it as long as they do not. Layout allocates, so a renderer that runs every frame should not lay out every frame. See Threads and Lifetimes.
Text that does change every frame, as a timer, a score or a number being
dragged, is laid out again into the layout that showed it before, with
:into, which reuses what the layout holds:
(setf layout (tr:layout-text (format nil "~,2F" seconds) font :into layout))
A simple line laid out again so allocates nothing, once the layout has held one as long: text without spans, horizontal and left to right, in one script beside common characters such as digits and punctuation, without tabs or paragraph separators, each character a grapheme of its own with a glyph in the first font, that fits the width, with a width and line spacing of single-floats, if given. It lands where a new layout would put it. Other text is laid out as anew, and the layout takes the result. A layout laid out again changes in place, so do it where no other thread reads it, as between the frames that draw it.