Why I created kas-text
September 2020
Existing libraries
This is going to be a very brief (and fairly naive) dive into text rendering and the state of available Rust libraries, written in easy English with minimal jargon.
The basics
Lets see. You're writing a small graphical game/tool/general UI and wish to print a simple string of text, say, "Hello World!" There are many APIs that can do this, but still several steps involved:
- Selecting a font: this might be embedded or the app may try to discover a
"standard" system font. The
font-kit
crate can discover system fonts. - Reading the font: there are a selection of libraries available, from the
low level
ttf-parser
to the mid-levelab_glyph
to more complex tool sets includingfont-kit
,rusttype
andfontdue
. - Translating the input string to a sequence of positioned and scaled glyphs
(layout): the above-mentioned
font-kit
,rusttype
andfontdue
can all accomplish this, along with the more specialisedglyph_brush_layout
. - Rasterising glyphs: this is the job of
glyph_brush
and, again,font-kit
,rusttype
andfontdue
. - Integrating rasterisation with the graphics pipeline: e.g.
gfx_glyph
andwgpu_glyph
do this job. Several high-level libraries (e.g.ggez
andamethyst_ui
) provide direct text support.
Of course, mostly you only need consider steps 1 and 5 and you're done.
Wrapping and alignment
Above, we considered only a short line of text: "Hello World!" Frequently you will have longer pieces of text that require wrapping (usually on word boundaries), and you may wish the result to be aligned to the left, centre or right, or even justified. Fortunately the above libraries already have you covered (aside from justified text, which is less often supported).
Text wrapping is a deceptively simple problem, but more on that in my next post.
Shaping
The above libraries all use a fairly simple layout system: match each char
to
a font glyph, place this at the previous glyph's "advance position", then if the
font provides kerning data for this pair of glyphs use it to adjust the gap
between the two. For English (and probably most languages) this is sufficient,
at least most of the time.
Shaping allows more complex glyph selection and positioning, and is an important part of real text libraries. See e.g. HarfBuzz on shaping and Wikipedia on Complex Text Layout (although the latter also concerns bidirectional text; more on that later).
Essentially, shaping takes a text string along with a font and spits out a sequence of positioned glyphs. This means that replacing the basic layout system used above with a text shaper should, in theory, be straightforward.
For Rust, we have the HarfBuzz binding harfbuzz-rs
, as well as two immature
pure-Rust libraries, rustybuzz
and allsorts
. Of the general-purpose text
libraries, only fontdue
includes shaping in its roadmap.
Right-to-left and bidirectional text
Several scripts, such as Hebrew and Arabic, are written right-to-left. Supporting such scripts generally also requires supporting embedded left-to-right fragments, and thus requires supporting bidirectional texts.
Unicode, as part of TR9, specifies how to determine the bidi embedding level and thus the text direction for each character in the source text, as well as how to re-order the input from logical order into display order. Unfortunately, integrating support for bidi texts into layout is not so simple (this is most of the topic of the next post).
The unicode-bidi
crate implements much of Unicode TR9, but on its own this
is insufficient. None of the above libraries support bidirectional text.
Formatting
Formatting may apply one of several effects to text. These include:
- adjusting the font size
- changing the font's (foreground) colour
- using a bold variant (or more generally, adjusting the weight)
- using an italic variant (either via use of another font variant or via slanting glyphs during rendering)
- drawing underline or strike-through
Some of the above font libraries have limited support for formatting specified via a list of text sections each with a font size and font identifier, and in some cases also colour information. The user is expected to translate formatted text from its source format as well as to select appropriate fonts.
Font properties and variable fonts
When mentioning font selection above, we glossed over a couple of issues. How does one select an appropriate italic or bold variant? Can these be synthesised if not provided?
The font-kit
crate can help with the above: it allows font selection by family,
weight and style. Some fonts are variable, meaning that glyphs can be
synthesised to the required weight (giving much more control than simply "bold"
or "normal"). The style allows selection of normal, italic (curved) or
oblique (slanted) variants.
I have not investigated this topic, but it appears that only font-kit
's
rasteriser API supports selection of weight or style; all other Rust libraries
select font only by path/bytes and variant index (for multiple fonts embedded
in a font pack).
Font fallbacks
What if your chosen font doesn't include all required glyphs, e.g. if you choose a font covering most European alphabets, but your user starts writing Korean? I believe that no Rust libraries have even started trying to address this problem.
The first sub-problem is an extension of font selection: choosing appropriate fallback font(s). CSS allows the user direct control here: see article. On Linux this is usually handled by Fontconfig.
The other sub-problem(s) concern integration: breaking text layout into multiple runs should your shaper not support multiple fonts (as HarfBuzz doesn't), then stitching the result back together.
KAS-text
The above libraries cover simple layout and rasterisation fairly well, but also leave a lot out, especially regarding complex layout and use of formatted input texts.
KAS-text attempts to fill this gap: translate from a raw input text to a
sequence of positioned glyphs, which can be easily adapted for use by
renderers such as wgpu_glyph
. Additionally, it exposes a stateful
prepared::Text
object, allowing fast re-draws and re-wrapping of a given
input text (though this stateful API may not be appropriate for all users).
Since this article is written after-the-fact,
kas-text
v0.1 already exists. You can
read its API docs here. v0.1 already supports
shaping (via HarfBuzz) and bidirectional text, which, to my knowledge, makes it
the first Rust library to support these features (on the full layout cycle from
raw text input to positioned glyphs), admittedly with imperfections.
Near-future plans include support for formatted text, including translation from Markdown or (simple) HTML input and mostly-automatic font selection. Other topics, such as vertical text, font synthesis, font fallbacks and emoticons, have not been planned but could eventually be added.
KAS GUI
The v0.5 release of KAS has integration with KAS-text, including a reasonably functional multi-line text-editor. Check it out or run yourself:
git clone https://github.com/kas-gui/kas.git
cd kas/kas-wgpu
cargo run --example layout --features shaping