tokens

package
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Aug 7, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package tokens holds the typed design values the whole system is styled from: the ColorTokens colour vocabulary — ADR-007's nine-step functional ramps, pinned role bases and thin semantic layer, derived from a brand seed by FromSeed — the Material Design 3 type roles, and the 4-pt spacing, radius, elevation and motion scales. The Tailwind eleven-stop scales survive only as optional named palettes, and the MD3-only colour names, deprecated aliases through v0.1.x, are gone as of v0.2.0: each was a fixed ramp step, named on ColorTokens.

Reach for it when you draw something yourself and want a value that matches the components around it — a pane background, a gap, a corner radius, an animation duration — instead of inventing a number. Components do not import this package for their values: they read the observables on a theme.Theme, and DefaultLight, DefaultDark, DefaultTypography, Comfortable, Spacing, Radius, Elevation and Motion are what those observables carry by default.

Every scale is a plain comparable struct of float32 device-independent pixels, except MotionScale, whose duration stops are time.Duration, and Typography, which also carries the font faces and the lazily built shaper its roles are shaped with. The package-level instances are variables rather than constants, so treat them as read-only: mutating one changes it for every consumer in the process. Copy and edit a value instead, which is how a custom theme is built.

Tonal elevation (goal G-E2, task E2.1): elevation is a surface step.

Per ADR-007, a raised surface separates from its ground primarily by colour — each elevation level fills with a step of the neutral ramp, one step deeper per storey — and only secondarily by a cast shadow. Because the light and dark ramps are paired scales (same step, same job), the same level reads as "raised" in both modes: a level-1 card is a light card on a lighter ground in light mode and a dark card on a darker ground in dark mode, with no mode-specific rule. The dp shadow survives as the secondary cue and is what pulse/depth still renders.

Seed-derived palettes. FromSeed turns one brand colour into ADR-007's complete paired light and dark ramp sets, and DefaultLight/DefaultDark are FromSeed of the default seed.

Derivation rules, and where each comes from:

  • The shared lightness scale is ADR-007's: CIELAB L* per step, measured by the D0.1 spike from the Claude Design reference project's own ramps. Light 100–900 = 97, 92, 85, 74, 63, 51, 39, 28, 6 — the measured 900 was L* 18, but D2.4's APCA gate deepened it: APCA's soft black clamp caps even pure black near Lc 92 over the L* 92 step-200 ground, so Lc ≥ 90 needs the 900 stop at L* 6 (min Lc 90.7 across the seven default ramps; L* 18 measured Lc 85–87). The dark scale is the paired scale measured from the same source's dark column (ADR-007's evidence table): 8, 13, 19, 30, 65, —, 82, —, 94; the 600 and 800 stops the table has no surface for are interpolated to 74 and 88 (the dark 900 already clears the gate at Lc 93–96, so it is untouched). Both scales are swept at constant OKLCh hue and chroma via color.Tone, which gamut-maps by chroma reduction (ADR-002).

  • Accent hues and chromas follow MD3's material-color-utilities conventions, which ADR-007 does not supersede, converted into the OKLCh chroma axis. The conversion anchor: the canonical seed #6750A4 has HCT chroma 48 and measures OKLCh chroma 0.1305, so one HCT chroma unit ≈ 0.00272 OKLCh chroma. Neutral = seed hue at chroma 0.010 (MD3's neutral 4; ADR-007's measured reference columns sit at 0.009–0.011). Secondary = seed hue at chroma 0.044 (MD3's 16). Tertiary = seed hue +60° at chroma 0.065 (MD3's 24). Primary uses the seed's measured hue and chroma unchanged.

  • The three status roles are hue-fixed, not seed-derived. A semantic colour must not rotate with the brand: a purple "success" says nothing. Each takes the OKLCh hue and chroma of a canonical Material colour, measured with this module's own converters and recorded here (measurements from F4.6; the error pair predates it, from D2.2):

    error hue 28.7°, chroma 0.178 — MD3's canonical error base #B3261E (its "hue 25, chroma 84"), L* 39.7 success hue 144.2°, chroma 0.162 — Material Green 500 #4CAF50, L* 63.98 warning hue 84.9°, chroma 0.172 — Material Amber 500 #FFC107, L* 81.52

    The palette anchor of each family is its 500 shade, so that is what is measured; only the hue and chroma are taken, since the depths come from the shared lightness scale like every other role. The three land 56–59° apart on the OKLCh hue circle — far enough that warning is not read as error at a glance, which is the whole point of a status colour.

  • Pins. The light primary base is the seed byte-for-byte (ADR-007: "the seed sits deep, so bases are pins" — reading it off the ramp would lighten it); only its alpha is forced opaque. The other light bases are their role's hue and chroma at tone 40, the depth MD3 pins accent bases at and the depth the default seed itself sits at (L* 40.08). Dark bases are the same hue and chroma re-toned to L* 82 — the dark scale's step-700 depth, right beside MD3's dark accent-base tone 80, making the dark pin byte-identical to its ramp's step 700. The D0.1 spike sat them at L* 65, the step-500 depth reproducing ADR-007's recorded dark fill #a690ea, but D2.4's APCA gate showed an L* 65 mid-tone is a ground no text can reach Lc 60 over (black tops out near Lc 52, white near 57), so the pins moved up two rungs — the default seed's dark primary is now #d0c4ff — and the solid state walk still lands on exact rungs (hover 800, pressed 900).

  • On-colours. Light bases sit at tone 40, so their on-colour is White (Lc ≥ 85, WCAG ≈ 6.4:1); dark bases sit at L* 82, so their on-colour is their own dark ramp's step 100 (Lc ≥ 73, WCAG ≈ 11:1). D2.4's APCA gate enforces ADR-007's Lc ≥ 60 on both.

FromSeedHighContrast (task E3.3) derives the increased-contrast variant from the same seed by the same machinery — it is a FromSeed option, not a third hand-written scheme. Three widenings, each computed against the APCA gate rather than guessed:

  • The 700 text step deepens to the default scale's 900 depth in both modes — light 700 L* 39 → 6, dark 700 L* 82 → 94 — so 700 text meets the same Lc ≥ 90 bar the default asks only of 900 (light min Lc 90.7, dark 93.0 across the seven ramps; APCA's soft black clamp caps lighter choices below 90, the same wall D2.4 hit). The 800 and 900 stops slide outward — light 3 and 0, dark 97 and 100 — keeping the ladder strictly monotonic and the 900 gate clear with margin (light Lc 92.3, dark 104.4). Steps 100–600 are the default scale unchanged: the grounds stay, the text pulls away.

  • Divider resolves from Neutral step 500 instead of 300: the separator jumps from the subtle-border rung to ADR-007's strong-border rung.

  • Each pinned base's on-colour is pushed further from its base. The dark pins' on-colours drop from their ramp's step 100 (L* 8, Lc ≈ 74 — just under the variant's Lc ≥ 75 floor) to tone 0, the scale's floor (Lc ≥ 76.3). The light pins keep White: it is already the far end of the axis and already clears the floor (Lc ≥ 85.7), so the pins do not move — the light primary base stays the seed byte-for-byte, the same contract as FromSeed.

Interaction states as step walks (ADR-007, task D2.3).

ADR-007 retires MD3's alpha state layers — the translucent 8%/12%/16% overlays of the role colour — in favour of states that are real, addressable colours a token sheet can emit. Two regimes:

  • Tinted (ground) surfaces walk the ramp by index: hover is one step past the component's ground, pressed and selected two (a card on 200 hovers on 300 and presses on 400). Because the light and dark ramps are paired scales — same step, same job — the SAME index walk holds in both modes: past 200 lies 300, a darker hover on a light ground and a lighter one on a dark ground, with no mode-specific rule.

  • Solid fills — the pinned role bases — walk "one / two steps from the pin toward 900" (ADR-007's surface mapping). Pins are off-ramp by design ("the seed sits deep, so bases are pins"), so a pin cannot walk by ramp index. The rule implemented here, chosen to keep "same step, same job" across modes: the pin's CIELAB L* is located on its role ramp's own measured L* ladder as a fractional step index; each state moves that index toward the 900 end by the tinted regime's walk (hover one rung, pressed and selected two), the target L* is read off the ladder by linear interpolation, and the colour is realized at the pin's own OKLCh hue and chroma by the tonal solver. The dark pin sits at the dark scale's step-700 depth by construction (D2.4 raised it from the spike's step-500 depth so its on-colour clears the APCA gate), so its hover lands at step-800 depth and its pressed clamps at the step-900 depth — exactly the walk a tinted surface performs — while the light seed pin (≈ step-700 depth for the default seed) darkens toward the 800/900 depths. In both modes the walk heads toward the 900 end of the paired scale: darker in light mode, lighter in dark mode.

Clamping: a walk past the ramp end clamps to the 900 stop — ground 800 pressed resolves to step 900, ground 900 hover stays at 900, and a solid walk never passes the 900 rung's depth. Clamping is friendlier than erroring: a component on the deepest ground still gets a stable colour.

The remaining states are not walks: disabled is an opacity (DisabledOpacity, MD3's 38%) applied to the state's normal colour; focus keeps the surface colour and adds the focus ring (FocusRing, Neutral step 500 — ADR-007's "strong border, focusable edge"); dragged resolves exactly as pressed.

Index

Constants

View Source
const (
	// ComfortableControlHeight is the default desktop control-height floor in
	// dp: a Comfortable control is at least this tall, and taller when its
	// content box needs it.
	ComfortableControlHeight float32 = 36
	// CompactControlHeight is the dense-mode control-height floor in dp. Every
	// control drawn in a Label or Body role clears it — see the table above —
	// so it is the floor that is least often the answer.
	//
	// The number is historical: it was measured off macOS's large control, and
	// the line-box rule that produced ComfortableControlHeight would have given
	// 32 here, not 28. Both facts are load-bearing and both are argued out
	// under "Compact's 28 dp is historical" above. Read that before changing
	// it — stacked rows are pinned to this value, not floored by it.
	CompactControlHeight float32 = 28
	// MinHitTarget is the pointer-target floor in dp for a *standalone*
	// control — one with space around it: button, checkbox, radio, text
	// field, the dropdown's closed trigger. Those extend their pointer area
	// to at least this on each axis, centred on the drawn control, whatever
	// the density.
	//
	// It is 44 dp, WCAG 2.5.5 Target Size (Enhanced), which is a AAA
	// criterion. It is not what stacked rows guarantee, and never was:
	// list rows, table rows and header cells, and open-dropdown option rows
	// are their own row height (28 dp at Compact) because extending one row
	// would steal its neighbour's slop. Those clear WCAG 2.5.8 Target Size
	// (Minimum) at 24 dp, the criterion that governs at AA. See "Pointer
	// targets: which WCAG level actually governs" above for the measured
	// per-row numbers.
	MinHitTarget float32 = 44
)
  • Compact's 28 dp is historical; the rule that made 36 would have said 32
  • Pointer targets: which WCAG level actually governs

Desktop density targets, measured 2026-08-05. This table is the justification for every number below it; later tasks (the Density token in E1.2, the component migrations in E1.3/E1.4) work from these values, so argue with the sources here rather than with the diffs there.

Three-way control metrics — shadcn/ui vs MD3 vs macOS (AppKit):

metric                  shadcn/ui                    MD3                            macOS (AppKit)
------                  ---------                    ---                            --------------
button height, default  36 px (h-9)                  40 dp (filled button)          24 pt regular, 28 pt large
button height, small    32 px (h-8; xs is 24 px)     — (no smaller desktop size)    20 pt small, 16 pt mini
input height            36 px (h-9)                  56 dp (filled text field)      24 pt (rounded-bezel field)
base radius             10 px; controls 8 px (md)    pill (buttons), 4 dp (field)   not published
stacked spacing         8 px label→control,          8 dp grid                      8 pt system spacing
                        28 px between fields

Sources (all fetched/measured 2026-08-05):

  • shadcn/ui: button.tsx size variants `default: "h-9 …"`, `sm: "h-8 …"`, `lg: "h-10 …"`, `xs: "h-6 …"`, base class `rounded-md`; input.tsx `"h-9 … rounded-md …"`; form.tsx FormItem `"grid gap-2"` (8 px label→control); field.tsx Field base `gap-3` (12 px) and FieldGroup `gap-7` (28 px between stacked fields); globals.css `--radius: 0.625rem` (10 px) with `--radius-md: calc(var(--radius) * 0.8)` = 8 px, the radius controls actually render with. Tailwind: h-9 = 2.25rem = 36 px, h-8 = 32 px. https://github.com/shadcn-ui/ui — apps/v4/registry/new-york-v4/ui/{button,input,form,field}.tsx and apps/v4/app/globals.css; https://ui.shadcn.com/docs/theming.

  • MD3: material-web design tokens v0.192 — md-comp-filled-button 'container-height': 40px, 'container-shape': corner-full; md-comp-filled-text-field 'container-shape': corner-extra-small-top (4 dp); md-sys-shape corner-extra-small 4 / small 8 / medium 12 / large 16 / extra-large 28 px. Filled/outlined text field container height is 56 dp per the m3.material.io text-field spec (the site is JS-walled; the 40 dp button height cross-checks against Flutter's generated token data, md.comp.filled-button.container.height = 40.0). MD3's minimum touch target is 48 dp. https://github.com/material-components/material-web — tokens/versions/v0_192/; https://github.com/flutter/flutter — dev/tools/gen_defaults/data/button_filled.json; https://m3.material.io/components/text-fields/specs.

  • macOS: measured directly against AppKit on macOS (Darwin 25.5.0) via fittingSize — NSButton (push bezel) mini 16 / small 20 / regular 24 / large 28 pt; NSTextField (rounded bezel, regular) 24 pt; stacked-control system spacing (constraint(equalToSystemSpacingBelow:multiplier:1) and NSStackView default spacing) 8 pt. Note the plan's "28 pt standard control" is NSControlSize.large — the size Apple uses for prominent buttons since Big Sur — while regular measures 24 pt. Apple's HIG publishes no per-size control heights for macOS, hence the direct measurement.

The picks:

  • Comfortable = 36 dp. The shadcn/ui default (button and input alike), sitting between macOS large (28 pt) and MD3's 40 dp — dense enough to read as a desktop app, generous enough to remain the default.
  • Compact = 28 dp. macOS's large control height and squarely between shadcn's sm (32 px) and xs (24 px): a native-feeling dense mode that stays above every AppKit regular-size control.

Why prism's existing 44 dp was rejected as Comfortable: 44 comes from touch guidelines — the WCAG 2.5.5 pointer-target minimum, next to MD3's 48 dp touch target — and every desktop column above lands well below it (shadcn 36, macOS 24–28; even touch-first MD3 draws its button at 40 inside a 48 dp target). It is a hit-target floor, not a visual control height, and it stays a hit-target floor: E1.2 keeps the ≥44 dp pointer target independent of density, so Compact shrinks the drawn control but never the clickable area. A control height is a floor, not a height. This is the word the table above was missing, and F4.4 found it by measuring rather than reading: a Compact button draws 29 px against a CompactControlHeight of 28, and it does so with an empty label, so no amount of text is to blame. The arithmetic is simply that a control is as tall as its content box needs, and never shorter than the density says:

height = max(ControlHeight, contentHeight + 2×PaddingY)

Where contentHeight is the type role's line height (see TextStyle.LineHeight and spectrum/typeset), the two terms are close enough that either can win:

control                role         line height   + 2×PaddingY   ControlHeight   drawn
-------                ----         -----------   ------------   -------------   -----
button, Comfortable    LabelLarge   20            36             36              36
button, Compact        LabelLarge   20            32             28              32
text field, Comfortable BodyLarge   24            40             36              40
text field, Compact    BodyLarge    24            36             28              36

Comfortable's 36 dp is exactly LabelLarge's line box plus its own padding, which is not a coincidence — the number was picked against a button.

Compact's 28 dp is historical; the rule that made 36 would have said 32

Compact's 28 dp is that same sum for a 16 dp line height — LabelMedium's, not LabelLarge's, and nothing in this system draws a button in LabelMedium. Read Comfortable's derivation as a rule ("the floor is the control's own line box plus its own padding") and apply it to Compact and the answer is not 28:

LabelLarge 20 + 2×6 = 32

32 is also shadcn/ui's sm button (h-8), so the evidence table above would have carried it without complaint. That is the figure the arithmetic wants, and it is written here so nobody has to re-derive it a third time. F4.4c found the discrepancy and documented it rather than changing the number; F5.6 re-opened the question and reached the same answer on purpose, for three reasons in descending weight:

  • 28 dp does not rest on the LabelMedium arithmetic and never did. It came from measurement — macOS's large control height, squarely between shadcn's sm (32) and xs (24) — and the LabelMedium coincidence was noticed afterwards, by F4.4. Correcting a derivation that was not the source of the number corrects nothing.
  • ControlHeight is a floor for controls but a *pin* for stacked rows. prism/list rows, cadence's table rows and header cells and sidebar items are ControlHeight tall exactly (see the row table below). Moving 28 to 32 is therefore not an arithmetic tidy-up; it is a visual change to every dense list and table in the system.
  • It would nearly erase Compact. A 32 dp Compact row against a 36 dp Comfortable one is an 11% difference where today it is 22% — in exactly the dense tables and lists Compact exists for.

So the two densities are derived by *different rules*: Comfortable from a type role's line box, Compact from measured native control heights. The asymmetry is intended, and this paragraph exists to say so rather than let the next reader find 28 ≠ 32 and assume it is a typo. What is not allowed is calling the result a height and then measuring something else.

The consequence worth saying out loud: controls in different type roles come out at different heights, and a Comfortable text field (40) is taller than a Comfortable button (36) because BodyLarge is a larger role than LabelLarge. Both are honest readings of the tokens. A design that wants them equal changes the roles or the padding, not the measurement.

Pointer targets: which WCAG level actually governs

MinHitTarget is 44 dp, and 44 dp is not the AA requirement. Two success criteria are in play and they are a whole conformance level apart:

criterion                            level   threshold   applies to
---------                            -----   ---------   ----------
WCAG 2.5.5 Target Size (Enhanced)    AAA     44×44 CSS px  every pointer target
WCAG 2.5.8 Target Size (Minimum)     AA      24×24 CSS px  every pointer target

(WCAG 2.2, https://www.w3.org/TR/WCAG22/#target-size-enhanced and #target-size-minimum. Both carry an inline/essential exception this system does not need to lean on.)

E1.3 extended the pointer area to 44 dp for standalone controls — button, checkbox, radio, text field, the dropdown's closed trigger — and deliberately not for stacked rows: list rows, table rows and header cells, and the open dropdown's option rows. Adjacent rows tile edge to edge, so slop granted to one row is stolen from its neighbour; the extension would not enlarge anything, it would only make the boundary lie about where it is. Rows rely on their full row width instead.

So the stacked-row targets are as tall as the row is, and F4.7 measured them at 1:1 rather than repeating the token (remember ControlHeight is a floor — max(ControlHeight, lineBox + 2×PaddingY) — so a row can draw more than the token says):

row                                     Comfortable   Compact   sizing
---                                     -----------   -------   ------
prism/list row (list.RowHeight)         36            28        pinned to ControlHeight
cadence/table body row and header cell  36            28        pinned to ControlHeight
cadence/sidebar item                    36            28        pinned to ControlHeight
prism/input dropdown option row         40            36        floor formula, BodyLarge

The narrowest of these is the 28 dp Compact row. 28 ≥ 24, so every row in the system clears 2.5.8 at AA; none of them reaches 2.5.5's 44, and F4.7 decided not to force them to. Flooring rows at 44 dp would erase Compact in exactly the dense tables and lists Compact exists for — a 44 dp "compact" row is 8 dp taller than a Comfortable one — which trades a real, everyday density benefit for a AAA criterion the system does not claim. An application that does target AAA sets Comfortable (36) and still does not reach 44 by row height alone; it needs a taller row of its own.

This is a scoped promise, not a weakened one: what narrowed in F4.7 was the documentation, which had claimed 44 dp for everything. Nothing about the drawn or clickable geometry changed.

View Source
const (
	WeightRegular = 400
	WeightMedium  = 500
)

CSS-style numeric font weights used by the default typography. The values follow the OpenType/CSS convention where regular is 400, so a zero weight always means "unset".

View Source
const DisabledOpacity = 0.38

DisabledOpacity is the fraction of full alpha a disabled element keeps — MD3's 38% disabled-content opacity, the exact value prism already renders (alpha 0x61 of 0xff). Disabled is an opacity, not a ramp step: a disabled surface keeps its colour and fades, per D2.3.

Variables

View Source
var (
	Slate = ColorScale{
		C50:  color.NRGBA{0xf8, 0xfa, 0xfc, 0xff},
		C100: color.NRGBA{0xf1, 0xf5, 0xf9, 0xff},
		C200: color.NRGBA{0xe2, 0xe8, 0xf0, 0xff},
		C300: color.NRGBA{0xcb, 0xd5, 0xe1, 0xff},
		C400: color.NRGBA{0x94, 0xa3, 0xb8, 0xff},
		C500: color.NRGBA{0x64, 0x74, 0x8b, 0xff},
		C600: color.NRGBA{0x47, 0x55, 0x69, 0xff},
		C700: color.NRGBA{0x33, 0x41, 0x55, 0xff},
		C800: color.NRGBA{0x1e, 0x29, 0x3b, 0xff},
		C900: color.NRGBA{0x0f, 0x17, 0x2a, 0xff},
		C950: color.NRGBA{0x02, 0x06, 0x17, 0xff},
	}
	Blue = ColorScale{
		C50:  color.NRGBA{0xef, 0xf6, 0xff, 0xff},
		C100: color.NRGBA{0xdb, 0xea, 0xfe, 0xff},
		C200: color.NRGBA{0xbf, 0xdb, 0xfe, 0xff},
		C300: color.NRGBA{0x93, 0xc5, 0xfd, 0xff},
		C400: color.NRGBA{0x60, 0xa5, 0xfa, 0xff},
		C500: color.NRGBA{0x3b, 0x82, 0xf6, 0xff},
		C600: color.NRGBA{0x25, 0x63, 0xeb, 0xff},
		C700: color.NRGBA{0x1d, 0x4e, 0xd8, 0xff},
		C800: color.NRGBA{0x1e, 0x40, 0xaf, 0xff},
		C900: color.NRGBA{0x1e, 0x3a, 0x8a, 0xff},
		C950: color.NRGBA{0x17, 0x25, 0x54, 0xff},
	}
	Red = ColorScale{
		C50:  color.NRGBA{0xfe, 0xf2, 0xf2, 0xff},
		C100: color.NRGBA{0xfe, 0xe2, 0xe2, 0xff},
		C200: color.NRGBA{0xfe, 0xca, 0xca, 0xff},
		C300: color.NRGBA{0xfc, 0xa5, 0xa5, 0xff},
		C400: color.NRGBA{0xf8, 0x71, 0x71, 0xff},
		C500: color.NRGBA{0xef, 0x44, 0x44, 0xff},
		C600: color.NRGBA{0xdc, 0x26, 0x26, 0xff},
		C700: color.NRGBA{0xb9, 0x1c, 0x1c, 0xff},
		C800: color.NRGBA{0x99, 0x1b, 0x1b, 0xff},
		C900: color.NRGBA{0x7f, 0x1d, 0x1d, 0xff},
		C950: color.NRGBA{0x45, 0x0a, 0x0a, 0xff},
	}
)

Optional named palettes, taken verbatim from the Tailwind CSS v3 default config. They are exactly that — palettes an application may reach for by name — and no part of the semantic layer resolves from them: since D2.2 every role ramp, pin and semantic colour derives from a seed (see FromSeed). Per ADR-002 the Tailwind values may survive only in this arrangement, never behind a role name.

View Source
var (
	// Comfortable is the default desktop density.
	Comfortable = Density{ControlHeight: ComfortableControlHeight, PaddingX: 16, PaddingY: 8}
	// Compact is the dense mode: smaller drawn controls, same hit target.
	Compact = Density{ControlHeight: CompactControlHeight, PaddingX: 12, PaddingY: 6}
)

The padding picks come from the same measured world as the control heights above:

  • Comfortable: shadcn/ui's default button is h-9 px-4 py-2 → 16 dp horizontal, 8 dp vertical, pairing with the 36 dp height.
  • Compact: shadcn's sm button drops to px-3 (12 dp); vertical scales with it on the 2:1 ratio the default keeps → 12 dp / 6 dp, pairing with the 28 dp height.
View Source
var DefaultLight, DefaultDark = FromSeed(DefaultSeed)

DefaultLight and DefaultDark are the canonical colour token sets: FromSeed(DefaultSeed), light and paired dark. The exact derived palette is recorded byte-for-byte in this package's golden test.

View Source
var DefaultSeed = stdcolor.NRGBA{R: 0x67, G: 0x50, B: 0xA4, A: 0xff}

DefaultSeed is the brand seed DefaultLight and DefaultDark derive from: #6750A4, the seed every ADR-002/ADR-007 measurement was made against.

View Source
var DefaultTypography = Typography{
	DisplayLarge:  TextStyle{Typeface: "Roboto", Weight: WeightRegular, Size: 57, LineHeight: 64, Tracking: -0.25},
	DisplayMedium: TextStyle{Typeface: "Roboto", Weight: WeightRegular, Size: 45, LineHeight: 52, Tracking: 0},
	DisplaySmall:  TextStyle{Typeface: "Roboto", Weight: WeightRegular, Size: 36, LineHeight: 44, Tracking: 0},

	HeadlineLarge:  TextStyle{Typeface: "Roboto", Weight: WeightRegular, Size: 32, LineHeight: 40, Tracking: 0},
	HeadlineMedium: TextStyle{Typeface: "Roboto", Weight: WeightRegular, Size: 28, LineHeight: 36, Tracking: 0},
	HeadlineSmall:  TextStyle{Typeface: "Roboto", Weight: WeightRegular, Size: 24, LineHeight: 32, Tracking: 0},

	TitleLarge:  TextStyle{Typeface: "Roboto", Weight: WeightRegular, Size: 22, LineHeight: 28, Tracking: 0},
	TitleMedium: TextStyle{Typeface: "Roboto", Weight: WeightMedium, Size: 16, LineHeight: 24, Tracking: 0.15},
	TitleSmall:  TextStyle{Typeface: "Roboto", Weight: WeightMedium, Size: 14, LineHeight: 20, Tracking: 0.1},

	LabelLarge:  TextStyle{Typeface: "Roboto", Weight: WeightMedium, Size: 14, LineHeight: 20, Tracking: 0.1},
	LabelMedium: TextStyle{Typeface: "Roboto", Weight: WeightMedium, Size: 12, LineHeight: 16, Tracking: 0.5},
	LabelSmall:  TextStyle{Typeface: "Roboto", Weight: WeightMedium, Size: 11, LineHeight: 16, Tracking: 0.5},

	BodyLarge:  TextStyle{Typeface: "Roboto", Weight: WeightRegular, Size: 16, LineHeight: 24, Tracking: 0.5},
	BodyMedium: TextStyle{Typeface: "Roboto", Weight: WeightRegular, Size: 14, LineHeight: 20, Tracking: 0.25},
	BodySmall:  TextStyle{Typeface: "Roboto", Weight: WeightRegular, Size: 12, LineHeight: 16, Tracking: 0.4},

	Code: TextStyle{Typeface: "Roboto Mono", Weight: WeightRegular, Size: 14, LineHeight: 20, Tracking: 0.25},

	Faces: append(roboto.FontFaces(), robotomono.FontFaces()...),
	// contains filtered or unexported fields
}

DefaultTypography is the canonical MD3 typography: Roboto throughout, the Material Design 3 sizes, and the official MD3 line heights and tracking. Display, Headline, Title Large and Body roles are regular weight; Title Medium/Small and the Label roles are medium. Code is BodyMedium's metrics on Roboto Mono, Roboto's companion mono face (G-F0); Faces carries the twelve Roboto faces first — the default family for text that names no typeface — then the four Roboto Mono faces Code resolves against.

Sixteen faces, and no symbol face: font/notosansmono is deliberately absent, because Shaper's system fallback already covers what it carries and more. Add it with WithFaces where there is no system to fall back on.

View Source
var Elevation = ElevationScale{
	Level0: 0,
	Level1: 1,
	Level2: 3,
	Level3: 6,

	Step0: 0,
	Step1: 200,
	Step2: 300,
	Step3: 400,
}

Elevation is the default scale instance.

View Source
var Motion = MotionScale{
	DurXFast:  50 * time.Millisecond,
	DurFast:   150 * time.Millisecond,
	DurNormal: 250 * time.Millisecond,
	DurSlow:   400 * time.Millisecond,
	DurXSlow:  500 * time.Millisecond,

	EaseStandard:           Bezier{P1: [2]float32{0.2, 0}, P2: [2]float32{0, 1}},
	EaseStandardAccelerate: Bezier{P1: [2]float32{0.3, 0}, P2: [2]float32{1, 1}},
	EaseStandardDecelerate: Bezier{P1: [2]float32{0, 0}, P2: [2]float32{0, 1}},

	EaseEmphasized:           Bezier{P1: [2]float32{0.2, 0}, P2: [2]float32{0, 1}},
	EaseEmphasizedAccelerate: Bezier{P1: [2]float32{0.3, 0}, P2: [2]float32{0.8, 0.15}},
	EaseEmphasizedDecelerate: Bezier{P1: [2]float32{0.05, 0.7}, P2: [2]float32{0.1, 1}},

	SpringDefault: Spring{Mass: 1, Stiffness: 80, Damping: criticalDamping80},
	SpringSnappy:  Spring{Mass: 1, Stiffness: 300, Damping: 22},
	SpringGentle:  Spring{Mass: 1, Stiffness: 20, Damping: criticalDamping20},
}

Motion is the default scale instance.

View Source
var Radius = RadiusScale{
	None: 0,
	Sm:   2,
	Base: 4,
	Md:   6,
	Lg:   8,
	Xl:   12,
	Xl2:  16,
	Xl3:  24,
	Full: 9999,
}

Radius is the default scale instance.

View Source
var Spacing = SpacingScale{
	S0:  0,
	S1:  4,
	S2:  8,
	S3:  12,
	S4:  16,
	S5:  20,
	S6:  24,
	S8:  32,
	S10: 40,
	S12: 48,
	S16: 64,
	S20: 80,
	S24: 96,
}

Spacing is the default scale instance.

View Source
var White = color.NRGBA{0xff, 0xff, 0xff, 0xff}

White is pure white. Unlike the named palettes above it is part of the semantic layer: FromSeed uses it as the on-colour over every light-mode pinned base.

Functions

func CriticalDamping added in v0.0.14

func CriticalDamping(stiffness, mass float32) float32

CriticalDamping returns the critical damping coefficient c = 2·√(k·m) for the given stiffness and mass: the smallest damping at which a spring reaches its target without overshooting.

func Disabled added in v0.0.10

func Disabled(c stdcolor.NRGBA) stdcolor.NRGBA

Disabled returns c with its alpha scaled by DisabledOpacity. Colours are non-premultiplied NRGBA, so scaling alpha alone is the whole rule; an opaque colour comes back at alpha 0x61.

func FontWeight added in v0.0.9

func FontWeight(weight int) font.Weight

FontWeight converts a CSS-style numeric weight, where regular is 400, to gioui.org's font.Weight, which counts in steps of 100 from regular at 0: FontWeight(400) is font.Normal and FontWeight(500) is font.Medium.

Types

type Bezier

type Bezier struct {
	P1, P2 [2]float32
}

Bezier holds the two inner control points of a cubic-bezier easing curve, equivalent to CSS cubic-bezier(P1.X, P1.Y, P2.X, P2.Y).

type ColorScale

type ColorScale struct {
	C50, C100, C200, C300, C400, C500, C600, C700, C800, C900, C950 color.NRGBA
}

ColorScale holds the eleven Tailwind shade stops for one hue family (50–950).

type ColorTokens

type ColorTokens struct {
	// Ramps holds the functional ramps, fully populated: nine steps per
	// role, generated on the shared lightness scale by FromSeed.
	Ramps RampSet

	// Pinned accent bases and their on-colours (ADR-007 "solid fill").
	Primary     color.NRGBA // pinned primary base — in a light scheme, the seed itself
	OnPrimary   color.NRGBA // text/icon over Primary
	Secondary   color.NRGBA // pinned secondary base
	OnSecondary color.NRGBA // text/icon over Secondary
	Tertiary    color.NRGBA // pinned tertiary base
	OnTertiary  color.NRGBA // text/icon over Tertiary
	Error       color.NRGBA // pinned error base
	OnError     color.NRGBA // text/icon over Error
	Success     color.NRGBA // pinned success base
	OnSuccess   color.NRGBA // text/icon over Success
	Warning     color.NRGBA // pinned warning base
	OnWarning   color.NRGBA // text/icon over Warning

	// The thin semantic layer (ADR-007's surface mapping). Background and
	// Text are pins — the "bg" and "text" bases E0.1 emits — while Surface
	// and Divider resolve from Neutral ramp steps at construction.
	Background color.NRGBA // pinned app background
	Text       color.NRGBA // pinned body text over Background
	Surface    color.NRGBA // card / raised surface — Ramps.Neutral.Step(200)
	// Divider is the subtle border / separator — Ramps.Neutral.Step(300),
	// except in the high-contrast variant, which resolves it from the
	// strong-border step 500 (see FromSeedHighContrast).
	Divider color.NRGBA
}

ColorTokens holds the colour vocabulary consumed by every Prism component: ADR-007's nine-step functional ramps, the pinned role bases, and a thin semantic layer resolved from ramp steps. Each "On" field is the recommended text/icon colour rendered on top of its companion pinned base.

The pinned bases exist because a brand seed rarely sits on the shared lightness scale: the pin reproduces the role's base colour exactly instead of reading a lightened approximation off a ramp step. Dark mode pins a dark-appropriate base rather than reusing the light pin.

The MD3-only field names — OnBackground, OnSurface, SurfaceVariant, OnSurfaceVariant and Outline — were deprecated aliases through v0.1.x and are gone as of v0.2.0. Each was a fixed resolution off the neutral ramp, so each is still reachable by asking the ramp directly: OnBackground was Text, OnSurface Ramps.Neutral.Step(900), SurfaceVariant Step(300), OnSurfaceVariant Step(700), Outline Step(500) — or FocusRing(), which is what Outline was actually used for.

func FromSeed added in v0.0.10

func FromSeed(seed stdcolor.NRGBA) (light, dark ColorTokens)

FromSeed derives the complete paired light and dark colour token sets from one brand seed: for every role a nine-step ramp on the shared lightness scale in both modes — the same step keeps the same job — plus the pinned bases, on-colours and semantic layer, per the rules in the file header. The light primary base is the seed itself, byte-for-byte (alpha forced opaque); every other value is generated.

DefaultLight and DefaultDark are FromSeed(DefaultSeed). Applications re-brand by calling FromSeed with their own colour and handing the pair to a theme.

func FromSeedHighContrast added in v0.0.14

func FromSeedHighContrast(seed stdcolor.NRGBA) (light, dark ColorTokens)

FromSeedHighContrast derives the increased-contrast variant of FromSeed's pair from the same seed: same roles, hues, chromas and pin depths, with the tone separation widened where it counts — the 700 text step deepened to the default 900 depth (Lc ≥ 90 where the default asks 60), Divider resolved from Neutral step 500 instead of 300, and the dark pins' on-colours pushed to the tonal axis floor (Lc ≥ 75 over their bases; the light pins keep White, which already clears that floor, so the light primary base is still the seed byte-for-byte). The full rules are in the file header; the derived default-seed variant is recorded in this package's high-contrast golden test.

It is the palette spectrum/system swaps in while the OS reports increased contrast (see system.HighContrastVariant).

func (ColorTokens) FocusRing added in v0.0.10

func (t ColorTokens) FocusRing() stdcolor.NRGBA

FocusRing returns the focus-ring colour: Neutral step 500, ADR-007's "strong border, focusable edge". It is the colour the Outline alias carried before v0.2.0 deleted it, and the one prism's button already strokes its ring with.

func (ColorTokens) SolidStateColor added in v0.0.10

func (t ColorTokens) SolidStateColor(role Role, state State) stdcolor.NRGBA

SolidStateColor resolves a solid fill: the role's pinned base under the given state, walking from the pin toward the 900 end of its ramp per the rule in the package-file header. Normal and focus return the pin itself; disabled returns the pin at DisabledOpacity. RoleNeutral panics: neutral has no pinned solid fill.

func (ColorTokens) StateColor added in v0.0.10

func (t ColorTokens) StateColor(role Role, ground int, state State) stdcolor.NRGBA

StateColor resolves a tinted surface: the colour of a component whose normal ground is the given ramp step (100–900 in hundreds), under the given state. Hover walks one step toward 900, pressed, selected and dragged two; walks past the ramp end clamp to step 900. Normal and focus return the ground itself (draw FocusRing for the ring); disabled returns the ground at DisabledOpacity. An out-of-vocabulary ground, role or state panics, matching Ramp.Step.

func (ColorTokens) SurfaceAt added in v0.0.13

func (t ColorTokens) SurfaceAt(level ElevationLevel) color.NRGBA

SurfaceAt resolves the surface colour of an elevated component: the fill of the given elevation level on t, per the default Elevation scale's step mapping. Level 0 is the Background pin over the step-100 ground; levels 1–3 fill with Neutral steps 200, 300 and 400.

D2.3's state walks compose on top with the level's step as the ground: hover on a level-1 surface is StateColor(RoleNeutral, 200, StateHover), i.e. Neutral step 300 in both modes, courtesy of the paired scales. A level-0 surface is the app background, which has no ramp ground; treat interactive regions on it as level-1 surfaces instead.

type Density added in v0.0.12

type Density struct {
	// ControlHeight is the minimum visual control height in dp
	// ([ComfortableControlHeight] or [CompactControlHeight]). It is a floor:
	// a control draws max(ControlHeight, contentHeight + 2×PaddingY), so a
	// content box taller than this makes the control taller. See the table
	// above the constants for which controls clear it and by how much.
	ControlHeight float32
	// PaddingX is the horizontal inner padding of a control in dp.
	PaddingX float32
	// PaddingY is the vertical inner padding of a control in dp.
	PaddingY float32
}

Density is one density setting: the drawn control height and its inner padding, all in dp. It is a comparable value struct like the other token types. The standalone-control pointer-target floor is deliberately a method, not a field — see Density.MinHitTarget — so no Density value can carry a shrunken hit target.

func (Density) MinHitTarget added in v0.0.12

func (Density) MinHitTarget() float32

MinHitTarget returns the standalone-control pointer-target floor in dp — the package const MinHitTarget, 44 dp, WCAG 2.5.5 Target Size (Enhanced). It is a method rather than a struct field, so it is structurally identical across every density: Compact shrinks the drawn control, never the clickable area of a control that has room to grow into.

It does not describe stacked rows. Read MinHitTarget before wiring this into anything that tiles.

type ElevationLevel

type ElevationLevel int

ElevationLevel selects an entry on the ElevationScale by name. The dp and step values for a given level are read from the Elevation instance.

const (
	Level0 ElevationLevel = iota
	Level1
	Level2
	Level3
)

type ElevationScale

type ElevationScale struct {
	// Shadow depths in device-independent pixels, following Material
	// Design 3 elevation levels 0–3. The secondary cue.
	Level0 float32 // 0 dp
	Level1 float32 // 1 dp
	Level2 float32 // 3 dp
	Level3 float32 // 6 dp

	// Surface-fill steps on the neutral ramp. Step0 is not a ramp step:
	// its zero value marks the Background pin — a level-0 surface is the
	// app's bg pin sitting over the step-100 ground.
	Step0 int // 0 — sentinel: the Background pin, not a ramp step
	Step1 int // 200
	Step2 int // 300
	Step3 int // 400
}

ElevationScale pairs, per level, the neutral-ramp step of the level's surface fill (the primary cue) with its shadow depth in dp (the secondary cue).

The ladder is four storeys, 0 through 3. MD3 levels 4 and 5 survived through v0.1.x as clamps onto level 3's step, so that call sites written against the MD3 numbering kept compiling; v0.2.0 deleted them, since a desktop surface has no six-storey stack to describe. A call site that named Level4 or Level5 meant "as raised as it gets" and should name Level3.

The LevelN fields carry the dp depths — pulse/depth's lookup and spectrum/export's --shadow-* table read them — and the StepN fields the paired surface steps. Prefer the Dp and SurfaceStep accessors over field access in new code.

func (ElevationScale) Dp added in v0.0.13

Dp returns level's shadow depth in device-independent pixels. An out-of-vocabulary level panics, matching Ramp.Step.

func (ElevationScale) SurfaceStep added in v0.0.13

func (e ElevationScale) SurfaceStep(level ElevationLevel) int

SurfaceStep returns the neutral-ramp step of level's surface fill, or 0 for a level whose fill is the Background pin rather than a ramp step (level 0 on the default scale). An out-of-vocabulary level panics, matching Ramp.Step.

type MotionScale

type MotionScale struct {
	// Duration stops — strictly increasing fastest → slowest, each pinned
	// to one MD3 duration role (see the table above).
	DurXFast  time.Duration // MD3 short1, 50 ms
	DurFast   time.Duration // MD3 short3, 150 ms
	DurNormal time.Duration // MD3 medium1, 250 ms
	DurSlow   time.Duration // MD3 medium4, 400 ms
	DurXSlow  time.Duration // MD3 long2, 500 ms

	// MD3 standard easing family: utilitarian transitions.
	// Accelerate is for exits, Decelerate for enters.
	EaseStandard           Bezier
	EaseStandardAccelerate Bezier
	EaseStandardDecelerate Bezier

	// MD3 emphasized easing family: expressive transitions. EaseEmphasized
	// is the documented single-bezier stand-in for MD3's two-segment path
	// (see above); the accelerate/decelerate pair is exact.
	EaseEmphasized           Bezier
	EaseEmphasizedAccelerate Bezier
	EaseEmphasizedDecelerate Bezier

	// Spring presets for the pulse physics path. Values track pulse's
	// current working springs until FX.2 retunes them (see above).
	SpringDefault Spring // critically damped, brisk: enter/exit scale
	SpringSnappy  Spring // slightly underdamped: button-press "pop"
	SpringGentle  Spring // critically damped, soft: large soft reveals
}

MotionScale holds duration stops, MD3 easing presets, and spring presets for animation tokens. See the mapping table above for where every value comes from.

func (MotionScale) Reduced added in v0.0.14

func (m MotionScale) Reduced() MotionScale

Reduced returns the reduce-motion variant of the scale: every duration stop is zero, everything else — easings and spring presets — is carried unchanged. It is what the theme's Motion field emits while the OS "Reduce Motion" preference is on (E3.2).

Zero durations are the whole contract. A duration-driven animation of zero duration is complete the moment it starts — pulse/motion's FramesAt(0, fps) is 0 frames — so a component that derives its frame count from the scale reaches its target on the first frame it draws: it snaps. (Watch one pulse edge: pulse/motion's Options treats a zero Frames as "use the default", so a caller that snaps must skip the primitive on a zero duration rather than construct one with Frames 0.)

The spring presets are deliberately NOT retuned. No finite spring completes in one frame, and a stiffness large enough to fake it (ω ≈ 600 rad/s for a 16 ms settle) is far outside the stability range of pulse/spring's explicit integrator at 60 Hz — it would oscillate or diverge, the opposite of reduced motion. A spring-driven component honours reduce-motion the same way a duration-driven one does: it reads the zero durations as the signal and jumps to its target instead of animating.

type RadiusScale

type RadiusScale struct {
	None float32 // 0 dp
	Sm   float32 // 2 dp
	Base float32 // 4 dp
	Md   float32 // 6 dp
	Lg   float32 // 8 dp
	Xl   float32 // 12 dp
	Xl2  float32 // 16 dp
	Xl3  float32 // 24 dp
	Full float32 // 9999 dp — pill / full-circle
}

RadiusScale holds border-radius stops in device-independent pixels, mirroring the Tailwind border-radius naming convention.

type Ramp added in v0.0.10

type Ramp [9]color.NRGBA

Ramp is one colour role's nine-step functional ramp per ADR-007. Steps run 100–900 in hundreds and the step number carries the meaning: 100–300 are tinted fills, hovers and subtle borders, 500 is the mid-value reference, 700–900 are text over tinted fills and pressed states. Index i holds step (i+1)*100; use Step to address a ramp by its step number.

Light and dark ramps are paired scales, not two role tables: the same step keeps the same job in both modes, so a component asking for neutral 200 gets a light card on a light ground and a dark card on a dark one.

func (Ramp) Step added in v0.0.10

func (r Ramp) Step(n int) color.NRGBA

Step returns the colour at step n, where n is one of 100, 200, … 900. Any other n is a programming error and panics.

type RampSet added in v0.0.10

type RampSet struct {
	Neutral   Ramp
	Primary   Ramp
	Secondary Ramp
	Tertiary  Ramp
	Error     Ramp
	Success   Ramp
	Warning   Ramp
}

RampSet holds the colour-role ramps ADR-007 defines. Neutral carries every surface, border and text shade; the accent ramps carry each role's tints and text shades, while the role's base colour is pinned separately on ColorTokens (see ColorTokens.Primary).

Error, Success and Warning are the semantic status roles. Unlike Primary, Secondary and Tertiary they do not rotate with the seed — a purple "success" would be useless — so each is derived at a fixed hue and chroma; see the seed.go file header for the three measurements.

type Role added in v0.0.10

type Role int

Role selects one of the colour-role ramps of a RampSet, and for the accent and status roles also the matching pinned base on ColorTokens. Neutral has a ramp but no pinned solid fill.

const (
	RoleNeutral Role = iota
	RolePrimary
	RoleSecondary
	RoleTertiary
	RoleError
	RoleSuccess
	RoleWarning
)

type SpacingScale

type SpacingScale struct {
	S0  float32 // 0 dp
	S1  float32 // 4 dp
	S2  float32 // 8 dp
	S3  float32 // 12 dp
	S4  float32 // 16 dp
	S5  float32 // 20 dp
	S6  float32 // 24 dp
	S8  float32 // 32 dp
	S10 float32 // 40 dp
	S12 float32 // 48 dp
	S16 float32 // 64 dp
	S20 float32 // 80 dp
	S24 float32 // 96 dp
}

SpacingScale holds named stops on the 4-pt grid, Tailwind-aligned. Field names match the Tailwind spacing key; values are device-independent pixels.

type Spring added in v0.0.14

type Spring struct {
	Mass, Stiffness, Damping float32
}

Spring holds the parameters of the textbook damped harmonic oscillator (m·ẍ = −k·x − c·ẋ) used by the pulse physics path: Mass is m, Stiffness is k, Damping is c. The three values are only meaningful together — critical damping (fastest settle, no overshoot) is c = 2·√(k·m), see CriticalDamping — so use a preset from MotionScale or set all three, never one.

type State added in v0.0.10

type State int

State enumerates the interaction states a component surface can be in. Hover, Pressed, Selected and Dragged resolve as step walks; Disabled is an opacity; Focus keeps the surface colour (the ring is FocusRing).

const (
	StateNormal State = iota
	StateHover
	StatePressed
	StateSelected
	StateDisabled
	StateFocus
	StateDragged
)

type TextStyle added in v0.0.9

type TextStyle struct {
	Typeface string
	Weight   int
	Size     float32

	// LineHeight is the height of one line box in dp — the whole box, not the
	// gap between lines, and not a multiplier. It means what CSS line-height
	// means: text in this role occupies LineHeight per line whatever its
	// glyphs measure, with the leading split evenly above and below the ink.
	// spectrum/export writes exactly this number into
	// `--font-<role>-line-height`, so the design-surface mirror and the Gio
	// rendering are stating the same fact.
	//
	// Handing it to gioui.org/widget.Label is not enough to get that, which is
	// the trap this comment exists for. Gio spends the line height on the gap
	// to the *next* line and gives the first line its own ascent plus descent,
	// so a MaxLines:1 label — nearly every control in this system — reports
	// the same size at any line height at all, and wrapped text lands one
	// deficit short of a whole multiple. Lay text out through
	// spectrum/typeset, which wraps widget.Label and adds the missing leading;
	// components in this organization all do.
	//
	// Zero means "no line height" and leaves the shaper's own default in
	// place. So does a negative one: widget.Label would install it and
	// gioui.org/text would then baseline each line above the one before it,
	// which is worse than any default, so spectrum/typeset drops it.
	LineHeight float32

	Tracking float32
}

TextStyle describes one Material Design 3 type role: the typeface to shape with and its metrics. Size, LineHeight and Tracking (letter spacing) are in device-independent pixels (dp); Weight is a CSS-style numeric weight where regular is 400 and medium is 500.

type Typography added in v0.0.9

type Typography struct {
	DisplayLarge  TextStyle
	DisplayMedium TextStyle
	DisplaySmall  TextStyle

	HeadlineLarge  TextStyle
	HeadlineMedium TextStyle
	HeadlineSmall  TextStyle

	TitleLarge  TextStyle
	TitleMedium TextStyle
	TitleSmall  TextStyle

	LabelLarge  TextStyle
	LabelMedium TextStyle
	LabelSmall  TextStyle

	BodyLarge  TextStyle
	BodyMedium TextStyle
	BodySmall  TextStyle

	// Code is the monospace style code renders in — markdown code blocks and
	// inline code spans. It is not one of the fifteen MD3 roles: MD3's 5×3
	// grid has no code role, so Code sits outside the grid as a sixteenth
	// style, carrying a body role's metrics on the mono face.
	Code TextStyle

	// Faces is the font collection both shapers build from. Every Typeface a
	// role names must appear in it, or text in that role falls back to
	// whatever face the shaper picks instead. Resolution is by Typeface name,
	// so the order of entries only matters as fallback for text that names no
	// typeface at all — the first faces are the default family.
	//
	// Use WithFaces to add to it; assigning here has no effect on a shaper
	// that is already built — and because the cache is shared between a value
	// and its copies, that includes one built through a different copy.
	Faces []font.FontFace
	// contains filtered or unexported fields
}

Typography holds one TextStyle per Material Design 3 type role.

func (*Typography) DeterministicShaper added in v0.4.0

func (t *Typography) DeterministicShaper() *text.Shaper

DeterministicShaper returns the shaper golden tests must draw with: Faces and nothing else, system fonts off, so that the same text shapes to the same pixels on every machine. It is built on the first call and cached separately from Shaper's, in the same cache every copy of the value shares. The single-goroutine rule under Shaper applies here too, and for the same reason.

A test that pins its faces this way says what it wants, which is stricter than inheriting a default — it cannot drift when the default changes, and it cannot pass here and fail on a machine with a different font set.

The pinning is real: a rune outside Faces shapes to the missing-glyph glyph rather than to whatever the machine happens to own. A test that legitimately draws such a rune adds the face that carries it instead of reaching for the platform's:

typ := tokens.DefaultTypography.WithFaces(notosansmono.FontFace())
shaper := typ.DeterministicShaper()

That keeps the test deterministic without making it blind. It does not make symbols acceptable in a golden image: the face serving a symbol is the machine-dependent thing goldens exist to avoid, so symbol coverage is asserted at the glyph level — the shaper resolved this rune to a real face — and never as pixels.

func (*Typography) Shaper added in v0.0.9

func (t *Typography) Shaper() *text.Shaper

Shaper returns the shaper applications should draw with: Faces first, then the platform's own fonts for anything Faces cannot serve. It is built on the first call and cached, and every copy of the value it was built through — including every copy an rx emission makes — hands back that same shaper.

The fallback is the point. Faces is Roboto and Roboto Mono, which between them carry no arrow, no box-drawing character and no dingbat, so a shaper confined to them draws a missing-glyph box — tofu — for text a real application genuinely receives. Leaving the system fonts on means text resolves: all of it, including the glyphs no embedded face was ever going to have.

What the platform serves therefore varies by machine, which is exactly why tests must not use this one. See DeterministicShaper.

One shaper, one goroutine

The shaper handed back is shared, and it is not safe for concurrent use. gioui.org/text says so itself: the same Shaper must not be used from different goroutines, and it panics on its internal map when it is. An earlier version of this comment promised the exact opposite — "safe for concurrent use from any number of goroutines" — and the per-copy cache that promise was written to justify is what stopped the cache from ever being read back.

Sharing one shaper is nevertheless right, because Gio renders on a single goroutine. The rx observables in this organization paint nothing: they assemble a forest of widgets that the frame event handler then lays out, on that one goroutine. A shaper therefore does not need to be per-value, per component or per emission — one shaper per face collection is what the toolkit expects, and the only arrangement that does not re-parse sixteen embedded faces and re-enumerate the platform's fonts on every theme change.

The rule that follows is short: call Shaper from the goroutine that runs the event loop and do not hand the result to another one. Copying the Typography does not buy you a second shaper. Use WithFaces, which allocates a fresh cache along with its wider collection.

func (Typography) WithFaces added in v0.4.0

func (t Typography) WithFaces(extra ...font.FontFace) Typography

WithFaces returns a copy of t whose collection is Faces followed by extra, with a fresh, empty shaper cache of its own, so the copy builds both shapers from the wider collection. The receiver is untouched, and neither slice is aliased.

It is the one line an application adds a face with, rather than rebuilding the collection by hand:

typ := tokens.DefaultTypography.WithFaces(notosansmono.FontFace())

Two callers want this. An application that cannot rely on system fonts — a container, a kiosk, anything shipping its own world — appends the optional symbol face, since the fallback Shaper counts on is not there to be had. And a test appends whatever face its subject legitimately draws from, then pins it with DeterministicShaper.

The extra faces go last, so they serve as fallback without displacing the default family: text naming no typeface still resolves to Faces[0]'s.

WithFaces is also the only supported way to get a second shaper out of this type. Copying a Typography no longer detaches its cache — that is the point of F5.1 — so a copy that must shape from a different collection has to say so here.

Call it while wiring, before shaping starts, on the goroutine that will do the shaping.

Jump to

Keyboard shortcuts

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