typeset

package
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 28, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package typeset lays a type role's text out in the line box that role names, rather than in the box its glyphs happen to ink.

It exists because gioui.org's text layout and a design system's typography mean different things by "line height". gioui.org/widget.Label passes LineHeight to the shaper, and gioui.org/text's calculateYOffsets baselines the first line at that line's own ascent and spends the line height only on the gap to the next one. The consequence is exact and easy to miss: a label with MaxLines 1 — which nearly every control in this system is — reports the same size at any line height at all. Measured on components/button's LabelLarge specimen at 14 dp: 17 px tall at line height 0, 20, 32 and 64 alike, and the rendered button byte-identical in all four.

A design system means the CSS thing. `line-height: 20px` on a one-line button makes the line box 20 px tall whatever the glyphs measure, the extra space split half above and half below the ink, and that is what theme/export already writes into `--font-<role>-line-height` for the design-surface mirror to consume. Without this package the Gio rendering and the CSS it exports disagree about the same token.

Layout is the fix, and it is a wrapper rather than a replacement: it lays the label out exactly as widget.Label would, then pads the result up to the line box and reports that. Callers keep MaxLines, Alignment, WrapPolicy and every other widget.Label field.

f := typeset.Font(style, font.Normal)
lbl := typeset.Label(style, 1)
dims := typeset.Layout(gtx, shaper, lbl, f, unit.Sp(style.Size), text, material)

The correction is a deficit, not a floor, so it is right for wrapped text too: Gio already spends the line height on each gap, so adding the one missing line height gives n lines a box of exactly n × line height.

The deficit is measured against the text being laid out, not against the face it names, because Gio takes a line's ascent as the maximum over that line's runs — a line holding a fallback run is taller than its primary face and needs less added, not the same. See Layout.

The same split applies one level up: a caller whose Min.Y floor exceeds the line box — layout.Flex hands an exact cell height down as a minimum — gets the ink centred within the floored height rather than pinned to the top of it. See Layout for the details.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Font

func Font(style tokens.TextStyle, fallback font.Weight) font.Font

Font builds the font.Font a text style shapes with. The style's typeface is honoured always; its weight is honoured when non-zero, and a zero weight — which is what an unset tokens.TextStyle carries — falls back to fallback. Pass font.Normal as the fallback unless the draw site has a weight of its own to keep.

func Label

func Label(style tokens.TextStyle, maxLines int) widget.Label

Label builds the widget.Label for a style at maxLines, with the style's line height installed as an absolute value (LineHeightScale 1) so the role's number is used verbatim rather than scaled by the face's own metrics. A line height that is not positive leaves both fields unset, which keeps the shaper's default.

The test is `> 0` rather than `!= 0` deliberately. widget.Label installs its LineHeight whenever it is non-zero and gioui.org/text then takes it as the whole line box, so a negative one baselines every line *above* the line before it and a wrapped label draws its lines on top of each other. There is no reading of a negative role line height that is better than none.

Set any other field — Alignment, WrapPolicy, Truncator — on the result before handing it to Layout.

func Layout

func Layout(gtx layout.Context, sh *text.Shaper, lbl widget.Label, f font.Font, size unit.Sp, txt string, material op.CallOp) layout.Dimensions

Layout lays txt out as lbl would and returns it in its line box: the same pixels, in dimensions tall enough for the line height lbl carries, with the leading split evenly above and below the ink and the baseline moved to match.

The correction is a no-op in two cases. An absolute line height smaller than the natural line of this text — an unset or negative one included — has no leading to distribute. And a label whose LineHeightScale is not 1 is asking for a height relative to the face's metrics, which the shaper already applies to every line including the first, so there is nothing missing to add. The floor handling below still applies on both paths.

The extra height is a single deficit, added once, not once per line. Gio already spends the line height on the gap between lines, so the only line short of its box is the first: adding lineHeight − naturalLine to a run of n lines makes it exactly n × lineHeight tall. The half above is rounded down, which is what keeps a centred label pixel-identical to the uncorrected one whenever its container was already taller than the ink.

The natural line is this text's, not this face's

naturalLine is measured from txt itself. Gio takes a line's ascent as the maximum over that line's runs, so a line carrying a fallback run — an arrow, a box-drawing character, anything the primary face has no glyph for — is taller than the primary face alone. Measuring a probe string instead made the deficit too large for exactly those lines: under the fallback shaper applications draw with, "arrows →←" came back 25 px tall where LabelLarge declares 20 and theme/export writes `line-height: 20`.

Constraints are applied once, to the corrected height

widget.Label constrains its own result, so adding the deficit on top of that would double-count: a label handed Min.Y == Max.Y — which is every Flexed child of a vertical layout.Flex — would report more than its slot. Layout therefore lays out under a relaxed Min.Y, corrects, and constrains the corrected size once with the caller's own constraints. The result fits the constraints it was given, which is what every other Gio widget promises, and callers no longer have to zero Constraints.Min to be told the truth.

A floor above the line box centres the ink too

When the caller's Min.Y exceeds the corrected line box — layout.Flex hands an exact cell height down to every rigid child as a minimum, so this is every label in an exact-height row — the surplus is split around the line box the same way the leading is split around the ink: half above, rounded down, half below, with the baseline tracking the ink. Left to widget.Label, that surplus would all land below the ink and the text would pin to the top of its cell; a parent aligning on Middle sees a child that claims the whole cell and has nothing left to centre.

The floor is handled on every path, including the two no-op cases above: the finding is about the caller's floor, not the line box, so a label with no line height of its own centres in an exact-height cell exactly as a corrected one does. A Max.Y below the corrected height, by contrast, keeps the ink anchored to the top and moves the baseline with the bottom edge.

The half-above is rounded down in both splits, so a layout the floor never exceeded stays pixel-identical to what it was.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL