Text measurement and drawing for Vibrant Gio,
a design system for native desktop applications on macOS, Windows and Linux,
written in pure Go on Gio — one level below the widget
layer, straight onto a *text.Shaper.
Gio's widget/material.Label is a widget: it takes the constraints it is
given, lays itself out, and returns its dimensions. That is the right shape
most of the time and the wrong one exactly when you are painting rather than
composing — a caption centred in a grid cell you computed, a row whose height
must not change when its text does, a pill of background colour sized to the
words inside it. Doing any of those through the widget layer means laying the
text out to find out how big it is, throwing that away, and laying it out again
inside the box you then built.
textdraw gives you the two halves separately. MeasureText shapes a string and
returns its size as an image.Point, drawing nothing. FillText paints a
string into an image.Rectangle you already decided on, at a fractional
alignment within it — (0, 0.5) for left-and-vertically-centred, (0.5, 0.5)
for dead centre — and returns nothing at all, because you already know where it
went. Text and Label wrap the same code as layout.Widgets for the cases
where you do want a widget.
The styling parameter is TextStyle: font, alignment, size in unit.Sp, line
limit, truncator and wrap policy. Where the values come from has two eras. The
frozen style module is a table of them,
and the support repositories' example programs still draw with
textdraw.FillText(gtx, shaper, style.H6, …). The workbench applications
instead derive theirs from the theme: a small per-app conversion turns a
spectrum/tokens.TextStyle role into a textdraw.TextStyle (see
todos/theme.go), so the typeface and sizes arrive through the theme and this
module still only ever sees the struct it defines.
Tier 0 of the stack — mvu → spectrum → prism → pulse → cadence → markdown —
a leaf that imports only Gio and golang.org/x/image. The
organization page has the full tier table.
Nothing inside the design system imports it: prism, pulse, cadence and markdown
draw their own text through Gio's widget layer.
style imports it for its TextStyle
table, and the callers are applications — a dozen example mains under
mvu/example, ivg/raster/gio/example, svg/driver/gio/example and
traer/gio, plus three of the seven
workbench applications: todos,
iconbrowser and mindchat.
This module is not deprecated, and the fate of style does not touch it.
ADR-003 froze style — F3.4 of the
org plan (planned) archives that
repository at v0.0.6 — but says nothing about textdraw, which stays a live
tier-0 module: no phase deprecates it, no phase deletes it, and MeasureText,
FillText and FillLabel have no replacement anywhere in the design system —
there is nothing else in the organization that measures a string or paints one
into a rectangle you chose. The workbench applications kept drawing through it
when F1 moved them off style; only the source of their TextStyle values
changed, from style's table to the theme's Typography roles.
go get github.com/vibrantgio/textdrawEvery module in the organization is on gioui.org v0.10.1 and Go 1.25.1.
One package, at the module root.
| Symbol | |
|---|---|
TextStyle |
Font, alignment, size (unit.Sp), MaxLines, Truncator and WrapPolicy. Everything else here takes one. |
MeasureText |
Shapes a string and returns its size as an image.Point. Draws nothing, allocates no ops. |
FillText |
Paints a string into an image.Rectangle at fractional alignment (ax, ay) within it. Returns nothing. |
Text |
FillText as a layout.Widget over the incoming constraints, returning Dimensions with a computed Baseline. |
FillLabel |
A rounded pill of fill sized to the text, with the string drawn on it in onFill. The pill tracks ax horizontally and is clamped inside the rectangle. |
Label |
FillLabel as a layout.Widget over the incoming constraints. |
Start, End, Middle |
Aliases of the gioui.org/text alignment constants, so a caller needs one text import instead of two. |
FontFace |
A type alias for gioui.org/font.FontFace — not a wrapper. A []textdraw.FontFace drops straight into text.WithCollection. |
EN_US, NL, ZH_CN, Default |
Prebuilt system.Locale values. All three are left-to-right; Default is EN_US. |
Measure, then place, then draw. This is list.go from
workbench/todos,
sizing a clickable row to one line of its Title style — the theme's TitleLarge
role converted to a textdraw.TextStyle, with the theme's cached shaper —
and vertically centring the item's text in it:
h := textdraw.MeasureText(gtx, typ.Shaper, typ.Title, "W").Y
size := image.Pt(gtx.Constraints.Max.X, h+gtx.Dp(Padding))
textdraw.FillText(gtx, typ.Shaper, typ.Title, image.Rectangle{Max: size}, 0.0, 0.5, textColor, item.Text)
return layout.Dimensions{Size: size}Measuring the literal "W" rather than item.Text is the trick worth
stealing: it gives every row the same height, so the list does not reflow when
a todo is renamed. MeasureText returns the height of one line for any
non-empty single-line string — at 16 sp with PxPerSp: 1, "W" measures
(16, 20), "Hello" measures (39, 20), and the empty string still measures
(0, 20).
ax and ay are fractions of the rectangle, not pixels, which is what makes
centring in a computed cell a single call. From iconbrowser/view.go —
(0.5, 0.5) for an empty-state notice in the middle of the pane, (0.5, 0.0)
for a caption centred under an icon:
textdraw.FillText(gtx, t.typ.Shaper, t.typ.Notice, image.Rectangle{Max: size}, 0.5, 0.5, p.Muted, notice)
captionRect := image.Rect(cell.Min.X, gtx.Dp(8)+iconPx+gtx.Dp(4), cell.Max.X, cellH)
textdraw.FillText(gtx, t.typ.Shaper, t.typ.Caption, captionRect, 0.5, 0.0, p.Text, IconTable[icon].Name)When you do want a widget, Text is the same drawing wrapped for layout —
here from traer/gio/gravity, an overlay title and an FPS readout pinned to
opposite corners by their alignment fractions alone:
layout.UniformInset(12).Layout(gtx, textdraw.Text(shaper, style.H3, 0.0, 0.0, Grey900, "Gravity Well"))
layout.UniformInset(12).Layout(gtx, textdraw.Text(shaper, style.H4, 1.0, 1.0, Grey900, fmt.Sprint(fps, "fps")))The *text.Shaper is always the caller's. In a Vibrant Gio application that
is the theme's — Typography.Shaper(), built once from the theme's faces and
cached in the value — which is what the workbench snippets above pass as
typ.Shaper. A program without a theme builds one per window at
layer-building scope (the example programs use
style.FontFaces()) and passes it
down; a self-built shaper owns glyph caches and is not safe for concurrent
use.
Read the canonical guide before writing code against this module — the module inventory with current tags, the application skeleton, MVU and rx semantics, typography, and the pitfalls that are not guessable:
https://raw.githubusercontent.com/vibrantgio/.github/master/llms.txt
AGENTS.md in this repository has the build and test commands.
Honest about what does not work yet. Every number below was measured against the built module.
TextStyleis superseded as a typography source, though this module is not. ADR-003'sTypographytheme token — a fullspectrum/tokens.TextStyleper MD3 role — is where type decisions live now, so thisTextStyleis a drawing parameter, not a place to define a type system: applications convert a theme role into one at the call site (todos/theme.gois the recipe) rather than declaring tables of them. style, the one in-org library that does declare such a table, is frozen and F3.4 (planned) archives it. The drawing functions keep their job; nothing replaces them.FillLabelandLabelhave no consumer anywhere in the organization. Not a module, not a demo, not a workbench application. They are the only two functions here that paint a background as well as glyphs, and nothing has ever called them, so their behaviour is unexercised. The pill's width istextWidth + ⅔ × lineHeight— for"Save"at 16 sp, measured 37 px wide and drawn 50 px wide — and that padding is added after the text was measured against the rectangle, so the pill can be up to two-thirds of a line height wider than the rectangle it was told to stay inside. The two clamps then fight: the first pushes the pill left to fit the right edge, the second pushes it back to the left edge, and it overflows on the right.FillLabelreturns the wrong dimensions. It returnslayout.Dimensions{Size: gtx.Constraints.Max}regardless of the rectangle it actually drew into, so a caller cannot learn how much space the label took.FillTextreturns nothing at all, by design — you already know the rectangle.MeasureTextis how you find out.EndandMiddleare exported and unused; so are the locales. EveryTextStyledeclared or derived in the organization — style's fourteen and the conversions the workbench applications build from theme roles — setsAlignment: textdraw.Start; alignment inside the rectangle is done with theaxfraction instead. Nothing anywhere referencesEN_US,NL,ZH_CNorDefault—gtx.Localecomes from the Gio window instead. The three locales are also allsystem.LTR, so the type has no RTL case to exercise, and no RTL text has ever been drawn through this module.- Measuring and drawing shape the string twice.
MeasureTextandFillTexteach callshaper.LayoutStringand walk the glyph iterator, so the measure-then-draw pattern this module is built around costs two full shaping passes per frame per string. There is no cached-layout API. In practice callers measure a constant like"W"and draw the real text, which hides the cost; measuring the real string in a long list would not. - Height counts only lines that end in a line break. Both functions
accumulate
dyfrom glyphs flaggedFlagLineBreak, so the returned height is a whole number of shaped lines and never a partial one. That is correct for Gio's shaper — it flags every line, including the last — but it means the measurement is line-quantised: at 16 sp, one line is 20 px, two are 40, and there is nothing in between. Textreports a baseline;Labeldoes not.TextcomputesBaseline: size.Y - firstLineBaseline, so it composes correctly in a baseline-aligned flex.FillLabelandLabelreturn a zeroBaseline.- There are no tests and no golden images.
go test ./...reports "no test files". This is the module the applications draw all their own text through, and none of its output is pinned.
MIT — see LICENSE.