← Font controls

Font controls reference

Issue: https://github.com/gpui-luma-dev/xgpui/issues/27

The web font-controls guide covers usage, support and core 3.0 migration. This page holds the implementation contract, measurements and individual API rationale.

Feature introspection

Resolve a font, then query the resolved face:

let id = text_system.resolve_font(&gpui::font("Inter"));
match text_system.opentype_features(id)? {
    gpui::FontQuery::Unsupported => { /* Hide the feature inspector. */ }
    gpui::FontQuery::Supported(features) => {
        // An empty list is a successful query with no GSUB/GPOS features.
        for feature in features {
            println!("{}: GSUB={} GPOS={}",
                feature.tag, feature.substitution, feature.positioning);
        }
    }
}

The result is a sorted union of the face's GSUB and GPOS FeatureLists. It records table provenance, merges duplicate tags, and returns errors for unreadable feature metadata or invalid font IDs on supporting backends. The reader validates feature records, not the complete shaping tables. It does not enumerate AAT features, named stylistic-set labels, allowed values, or script/language associations. Feature presence does not establish that a feature applies to a particular script, language, text, or variation instance. It describes this face only; fallback runs can use other faces. As with other font queries, font IDs belong to the text system that created them.

Apple's font-kit backend, Parley, and DirectWrite implement feature and axis queries. DirectWrite static axes require the runtime IDWriteTextLayout4 probe to succeed. Native/Parley font-controls labs and tests are verified on macOS and Windows. Parley in Wasm supports the same queries and static axes, exposed in the Wasm inspector. Cosmic now implements table access for resolved font files, including collection faces, and reports feature/axis inspection support. Its arbitrary static axis requests remain rejected. The default Cosmic/Canvas web wrapper still reports inspection as Unsupported; it does not expose the new native Cosmic hook. TextSystem::font_capabilities() reports support explicitly. Unsupported backends reject nonempty axis requests in their font_id entry point rather than silently pretending to apply them. Note that GPUI's existing resolve_font convenience method can fall back on resolution errors; callers should check capabilities before presenting axis controls. Err, Unsupported, and Supported(vec![]) must remain distinct in inspectors.

Use existing FontFeatures to request settings. Values are numeric OpenType parameters, not exclusively booleans. For example, ("cv02", 2) must retain 2; 0 disables a feature, while nonzero values can select different alternates. Enumeration does not change the descriptor or enable any features. Backend-specific shaping limitations still apply.

Precise variation values

FontTag accepts exactly four printable ASCII bytes, including spaces. Use FontTag::{WEIGHT, WIDTH, SLANT, OPTICAL_SIZE, ITALIC} for the registered axes. Constants avoid parsing in normal application code. Custom tags still use validated FromStr, and serialized tag strings are unchanged.

FontVariations::new accepts (FontTag, f32) pairs in font design units:

Font::variations now carries these values through Apple, Parley, and the DirectWrite implementation. For normal element styling, use Styled::font_variations(...) or Styled::font(...); TextStyle::font_variations propagates them into text runs. Values serialize as a JSON map, such as {"slnt": -10.125, "wght": 500.25}. Deserialization validates every entry before canonicalizing duplicates.

let mut font = gpui::font("Roboto Flex");
font.variations = gpui::FontVariations::new([
    (gpui::FontTag::SLANT, -10.125),
    (gpui::FontTag::WEIGHT, 500.25),
])?;

Static-axis resolution rules

Apple, Parley, and the DirectWrite implementation use the following rules in shaping, metrics, glyph bounds, caret/selection geometry, fallback, and rasterization:

  1. Use existing family, weight, and style selection to choose the face. Explicit axes do not select another family or substitute for static-face matching.
  2. On that face, apply explicit coordinates after implicit weight/style axis settings. Explicit wght, ital, and slnt override the corresponding implicit coordinate. Other implicit coordinates remain as before.
  3. Clamp each supported axis to that resolved face's advertised minimum and maximum. Unsupported tags are ignored for that face; they do not fail font selection. Preserve the original author settings separately from effective coordinates so fallback faces can apply their own support and ranges.
  4. Apply this process independently to every fallback face. Do not transfer normalized coordinates between different faces. Resolved instance identity must describe the coordinates actually used throughout the pipeline.
  5. Explicit opsz is in font design units and follows the same rules. There is no new automatic optical sizing. Preserve existing backend defaults when opsz is omitted. Automatic sizing requires a later size-dependent identity design, including units, precedence, clamping, and fonts without opsz.
  6. Keep precise author coordinates distinct from any future preview policy. Preview quantization must occur before resolution and affect shaping and rendering together. The optional extension preview policy is described below; global budgets and frame-safe eviction remain separate.

Apple keeps requested descriptors separate from resolved native identity, then applies author coordinates to resolved fallback runs before reshaping. Its cascade now uses actual descriptors for in-memory user fallback fonts, which family-only cascade descriptors could not reliably resolve. Parley passes explicit variations through its shared flat-line/paragraph style builder and retains the resulting normalized coordinates in its existing face registry. Retained IDs are never recycled. Layout and raster keys continue to use FontId; no duplicate axis arrays were added to glyph or layout cache keys.

DirectWrite first resolves the native faces using normal selection/fallback. For each variable run, it clamps explicit coordinates against that face's font resource, creates an instance collection, and applies effective coordinates to an IDWriteTextLayout4 range. The final callback retains the actual shaped face for metrics/rasterization instead of selecting another face by family name. Numeric typography settings remain associated with the requested descriptor. In-memory user fallbacks use a collection containing custom and system fonts; owned font buffers remain alive for the text-system lifetime. Static faces ignore unsupported axes. The implementation and its five fixture tests have been cross-checked for MSVC but not executed on Windows yet.

Pinned fixtures and validation

Tests use repository font bytes, without host-font discovery for metadata:

FixtureLicenseSHA-256
gpui/crates/gpui/assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttfSIL OFL, adjacent license.txt975dcda37d80f038dcd143c22e33ca2d97a0cc5a929aace1c749153b0fe1afa5
extensions/crates/xgpui-parley/tests/fonts/InterVariable.ttfSIL OFL, adjacent Inter-LICENSE4989b125924991b90d05b2d16e0e388c48f7d5bb8b30539bbf9c755278d0ccaf

Core tests cover canonical settings, signed precision, invalid values/tags, empty results, unsupported queries, malformed/truncated feature metadata, merged GSUB/GPOS provenance, numeric feature serialization, and real feature lists. Apple and Parley tests exercise resolved-face queries and invalid IDs; Parley also exercises numeric controls through shaping. Static-axis tests cover negative slant with Roboto Flex, axis-order equivalence, explicit weight precedence, range clamping, instance isolation, fallback coordinates, retained raster output, size agreement with explicit optical size, and paragraph wrapping/caret/selection geometry. The Roboto Flex source revision, license, and SHA-256 are recorded beside the fixture in the Parley fonts README.

Preview policy and measured baseline

The extension-owned xgpui_parley::VariationPreview accepts a fixed base font, per-axis ranges and intervals, and an explicit maximum number of admitted preview descriptors. It snaps coordinates before font resolution. Once the limit is reached, old settings still work and new settings return an error; the caller can keep displaying its last successful preview. Author coordinates remain precise and must be committed separately after editing. Keep the same controller for the inspector's lifetime. This controller does not evict IDs or atlas tiles and does not claim a global CPU/GPU memory bound.

A debug CPU diagnostic on this macOS/Apple Silicon host ran an 8 × 8 × 4 wght/wdth/slnt sweep three times, using the pinned Roboto Flex font, Hamburgefonts 012345, 28px, scale 1, and one subpixel variant. Raster masks were built once per distinct raster key. Coarse preview intervals (400/50/5 design units) illustrate a policy; these are not recommended production defaults.

PolicyRequested descriptorsRegistered faces/aliasesRaster keysMask payload bytesFirst sweepThree sweeps
Precise2575155,1201,361,3811,145ms1,460ms
Coarse preview2857540150,204238ms508ms

Counts include initial probes. Repeated identical sweeps stabilized after the first sweep in both cases. Timings are local debug measurements, not production performance targets. These CPU counts do not include all GPUI caches; mask payload bytes are not atlas allocations or GPU memory. The diagnostic is reproducible with:

cargo test --manifest-path extensions/crates/xgpui-parley/Cargo.toml --lib multi_axis_sweep_baseline --features native-test,gpui-platform/runtime_shaders --offline -- --ignored --nocapture

A separate native Apple/Metal diagnostic uploads the same specimen to a real atlas. It uses native CoreText masks, so its timings are not comparable to the Parley CPU-only timings above. GPU access required running outside the sandbox.

PolicyRequested instancesLive tilesAllocated pagesMTLResource::allocatedSize totalFirst sweepThree sweeps
Precise2565,12044,325,376 bytes381ms510ms
Coarse preview2754011,081,344 bytes91ms212ms

Repeated sweeps stabilized. Removing all keys with no scenes or GPU commands referencing them left zero active pages and tiles. A retained texture handle still held 1,081,344 allocated bytes until dropped: active allocator counters are not the same as total resource lifetime. This diagnostic does not measure rendered frames, completion fences, driver-level reclamation, or cached-scene invalidation. It exposed and now guards against loss of variation coordinates when CoreText resizes an embedded font with implicit optical sizing. Effective coordinates and the sizing policy are retained explicitly in native identity for the static-axis path. Shaping/drawing preserve these coordinates; bounds and metrics use the same underlying instance. Empty-variation descriptors retain the existing sizing path, separately from static-axis instances.

cargo test --manifest-path gpui/crates/gpui-apple/Cargo.toml --lib static_axis_sweep_measures --features font-kit,runtime_shaders --offline -- --ignored --nocapture

Core GPUI public API review ledger

The user approved the API changes below, including the backend table-access hook. Raw-table parsing is private inside core GPUI. Default inspection methods read table bytes through PlatformTextSystem::font_table; Apple, DirectWrite, and Parley implement only native table access. There is no helper package, copied parser source, synchronization script, or public parser API.

Public APICompatibilityWhy it is in core / extension alternative
Font::variations: FontVariationsBreaking: exhaustive Font literals and patterns require updatingOrdinary GPUI text descriptors must carry coordinates into every backend. An extension-only descriptor cannot pass through existing Font-based styling and runs without another core hook.
TextStyle::font_variations: FontVariationsBreaking: exhaustive style literals and patterns require updatingCarries inherited coordinates into font() and to_run(). Could be omitted only if ordinary element styling does not expose axes.
Generated TextStyleRefinement::font_variations: Option<FontVariations>Additive setting and serialized key; breaking for exhaustive literals/patternsRequired by normal style refinement/inheritance. Missing serialized values remain None.
Styled::font_variations(FontVariations)Additive default trait methodConvenience over the style field; can be moved to an extension trait independently.
FontTagAdditive type; FromStr, Display, Clone, Copy, Debug, equality, ordering, hash; as_bytes()Used by core variation settings and inspection results. Could move with the owning contract types, at the cost of a core dependency on that crate.
FontTag::WEIGHTAdditive associated constant for wght; no source/serialization changeTyped spelling for the registered weight axis; belongs with the existing core tag type.
FontTag::WIDTHAdditive associated constant for wdth; no source/serialization changeTyped spelling for the registered width axis; same ownership.
FontTag::SLANTAdditive associated constant for slnt; no source/serialization changeTyped spelling for the registered signed slant axis; same ownership.
FontTag::OPTICAL_SIZEAdditive associated constant for opsz; no source/serialization changeTyped spelling for explicit optical size; does not enable automatic sizing. Same ownership.
FontTag::ITALICAdditive associated constant for ital; no source/serialization changeCompletes the five registered axes; italic design selection is distinct from slant. Same ownership.
FontVariationsAdditive type; new(), settings(), default, clone/debug, equality/hash, serde, schema, TryFrom<BTreeMap<String,f32>>, conversion back to that mapNeeded by the new core descriptor/style fields. Validated immutable storage protects cache equality/hash invariants. Could move to a small shared font-types crate; not dependent on Parley.
FontQuery<T> with Unsupported / Supported(T)Additive enum, clone/debug/equalityMakes unsupported inspection distinct from success and failure. Could move to an inspection extension if the query methods move.
OpenTypeFeatureInfo with public tag, substitution, positioningAdditive type, clone/debug/equalityCross-backend inspection return type. Can move with an extension-owned inspector interface.
VariationAxisInfo with public tag, min_value, default_value, max_value, hiddenAdditive type, clone/debug/partial equalityCross-backend axis inspection return type. Can move with an extension-owned inspector. Localized names are not included.
FontCapabilities with public feature_introspection, axis_introspection, static_variationsAdditive type, default/copy/clone/debug/equalityMakes incomplete backend support visible. Could move with an inspector interface, but static support still needs a discoverable contract.
TextSystem::opentype_features(FontId)Additive inherent methodEntry point on the text system applications already own. Moving to an extension trait requires a backend inspection handle or another core access hook.
TextSystem::font_axes(FontId)Additive inherent methodSame inspector-handle alternative.
TextSystem::font_capabilities()Additive inherent methodSame inspector-handle alternative.
PlatformTextSystem::opentype_features(FontId)Additive default trait method; existing implementations compileDefault implementation reads GSUB/GPOS via font_table and parses privately in core. Backend extension point. Can move to a separate optional inspector interface with corresponding access/ownership plumbing.
PlatformTextSystem::font_axes(FontId)Additive default trait methodDefault implementation reads fvar via font_table and parses privately in core. Same optional inspector-interface alternative.
PlatformTextSystem::font_capabilities()Additive default trait methodDefaults to no new support; implementations opt in explicitly. Same interface alternative.
PlatformTextSystem::font_table(FontId, FontTag) -> Result<FontQuery<Option<Vec<u8>>>>Additive default trait method; defaults to Unsupported, so existing implementations compileBackend hook for resolved-face table bytes. Supported(None) means absent, Some(bytes) means read successfully, and errors mean invalid ID/read failure. Lets core parse metadata privately. An extension-owned reader would require another access hook to backend font handles.
xgpui-windows --no-default-featuresFeature opt-out behavior fix; ordinary defaults unchangedThe dependency now has default-features = false; the existing default feature forwards gpui/default. This lets explicit opt-out avoid the core manifest compiler. Cargo feature behavior belongs to the backend crate, not an extension.

No existing FontFeatures signatures or serialized behavior changed; its numeric values were already supported. Apple implements the trait methods without new public inherent methods or types. The preview controller and its constructor, font(), and admitted_instances() are extension APIs, not core APIs.

Migration for exhaustive literals

Prefer gpui::font(family) or Font { ... , ..Default::default() }. Existing exhaustive font literals need variations: Default::default(). Existing exhaustive text-style literals need font_variations: Default::default(); refinement literals need font_variations: None. Exhaustive patterns can name the new fields or use ... Empty coordinates preserve existing axis behavior. The in-memory Apple fallback correction described above is a separate behavior fix: requested embedded fallbacks can now resolve to their actual font.

Released scope and follow-ups

The release includes Apple, DirectWrite and Parley static axes, inspection, and measured previews. macOS and Windows labs and unit tests are confirmed working. The public API is reviewed and raw parsers remain private in core. Follow-ups:

Those follow-ups retain these gates:

Do not advertise globally bounded scrubbing based on these isolated diagnostics. Later stages are tracked in the linked issues.

Verification on the implementation host

Tests used the existing runtime-shader feature to avoid writing Metal compiler cache files outside the sandbox. No dependencies were added.

Windows implementation and API impact

The Windows backend implements the existing reviewed interfaces; this change adds no further core GPUI public types, fields, or methods. Its internal native face handling and metadata queries are new. The xgpui-windows feature opt-out fix described above changes --no-default-features behavior deliberately; ordinary default builds keep forwarding GPUI's defaults.

For repeatable Windows and macOS commands, expected output, fixture tests, and patch transfer instructions, see font-controls testing.

Interactive text-lab inspector

The comparison workload now includes application-local live sliders for wght, wdth, slnt, and opsz on each supporting backend. Presets, keyboard and accessibility actions share the same VariationPreview path as pointer drags. A fixed 512-descriptor admission limit holds the previous preview on exhaustion; there is no eviction, font-ID reuse, or unbounded reset. The specimen font and its advance measurement use the same effective settings. No new core GPUI public API was added for these controls.

Release versions

The combined font-controls and GPUI 1.27.0 release uses these selected versions and exact internal dependency requirements. No new crate is published; the parser module is private inside xgpui.

CratePreviousVersionRationale
xgpui2.0.13.0.0Breaking fields on Font, TextStyle, and TextStyleRefinement, plus GPUI 1.27.0 keystroke, window-text and platform contract changes.
xgpui-apple3.0.14.0.0Static axes and introspection plus GPUI 1.27.0 integration: renderer constructors become fallible, the factory drops unused arguments, and thermal observation moves to process_info.
xgpui-windows2.0.12.1.0New static axes and introspection capabilities; existing public signatures remain intact.
xgpui-parley0.3.10.4.0New axes/introspection capabilities and additive VariationPreview API; no removed APIs.
xgpui-wgpu2.0.12.1.0Adds Cosmic feature/axis inspection; arbitrary axis application remains rejected.
xgpui-web2.0.12.0.2Reject unsupported explicit axes and update core/backend pins.
xgpui-macos3.0.14.0.0Typed AppKit integration, macOS 12 minimum, and a fallible constructor on the public headless-renderer re-export.
xgpui-ios1.26.31.26.4Core/Apple pins and upstream UIKit initialization updates.
xgpui-linux2.0.12.0.2Core and wgpu dependency-pin updates only.
xgpui-platform2.0.12.0.2Matching core and backend dependency-pin updates only.
xgpui-media0.5.10.5.2Matching core/backend/shader dependency-pin updates only.
xgpui-shader0.4.10.4.2Matching core/backend dependency-pin updates only.
xgpui-gamut0.3.10.3.2Matching core/backend/shader dependency-pin updates only.
xgpui-canvas0.3.10.3.2Matching core/shader dependency-pin updates only.
xgpui-svg0.3.10.3.2Core dependency-pin updates only.

xgpui-color remains 0.3.0 and xgpui-macros remains 1.0.1. The repository's versioning policy does not automatically propagate major bumps to dependent crates. Apple and macOS receive majors for their changed public constructors; the other dependents retain their public members/signatures. Their GPUI inputs, outputs, traits, and re-exports now resolve to the matching 3.0 type universe, so consumers must upgrade the pinned stack together; mixing this release family with core 2.x is unsupported. The exhaustive-literal migrations above remain necessary wherever applications directly construct the changed core types.

Ergonomics follow-ups

The standard axis constants are implemented. The following are design follow-ups, not callable APIs in this release:

Each additional core API requires its own compatibility and ownership review.

Cosmic inspection milestone

CosmicTextSystem implements the existing PlatformTextSystem::font_table hook using the selected font's bytes and collection index. ttf-parser::RawFace reads the directory and table extent; the core's existing private parsers interpret GSUB/GPOS and fvar. Advertised unreadable tables return errors; absent tables return Supported(None). Invalid IDs return errors without indexing panics. No new core public type, field or method is introduced.

The backend reports feature and axis inspection, while static_variations stays false. The text lab displays inspection independently from axis editing. Tests cover pinned static/variable fonts, a two-face collection, invalid IDs, missing and corrupt table reads, and continued rejection of explicit axes.

Cosmic 0.19 still lacks a general arbitrary-axis shaping input in its attribute contract. Static-axis support remains a separate change spanning shaping, resolved-instance identity, fallback, metrics and Swash rasterization; painting with varied coordinates alone would be incorrect. Linux CI also runs the Parley library, native-host construction/axis smoke test and shared slider tests. Interactive Wayland/X11 and Linux IME/accessibility acceptance remain separate.

Validation at commit 8359fe8 passed on Ubuntu: 29 Cosmic tests, 36 active Parley library tests, the native-host construction/axis smoke test and shared slider tests. The complete validation run also passed native macOS/Windows, experimental iOS simulator and Wasm checks. The Linux host build exposed a stale DMA-BUF completion-guard call; the private guard now retains either staging allocations or imported textures, preserving resource lifetime and admission-release order. This adds no public API and is included in xgpui-wgpu 2.1.0; no additional version increment is needed for this fix within the same release.

Testing font controls

Run commands from the repository root. The lab bundles licensed fonts; no font installation is needed. See the font-controls guide for API usage, backend support and the core 3.0 migration.

Wasm inspector

Open the font-controls demo from the website's Text gallery. Use its WebGPU and WebGL2 launch links to check both renderers. The real GPUI slider component and Parley/Skrifa pipeline execute in Wasm; the specimens are not CSS text. Check all four sliders, keyboard limits, presets, measured advances, and axis/feature metadata.

To build and preview the site locally:

just pages-demos
just docs
just pages-serve

Open /examples/extensions/?demo=font-controls on the local server. The default GPUI web text backend remains separate from this demo's Parley backend.

Native and Parley comparison

On macOS or Windows:

just text --backend both

Two windows compare native CoreText/DirectWrite with Parley. Each has independent weight, width, slant and optical-size sliders. Use the same presets to compare matching settings. Drag a track or focus a slider and use arrows, Shift for larger steps, Home and End. Confirm both specimen sizes, wrapping and measured advance update together, without clipping. Default restores the original values.

The inspector uses steps of 10/1/0.25/1 design units and admits 512 combinations per window. When full, it holds the last preview and displays an explanation. Previously admitted settings and Default remain usable. Restart for a fresh session. This is not a global CPU/GPU memory budget or an eviction policy.

Scroll down for the static samples:

Available axis ranges and GSUB/GPOS tags appear with the samples. Presence does not guarantee an effect on every string. Check that the editor still supports selection and typing. Minor native/Parley or display-dependent raster differences need not indicate a coordinate mismatch; compare the displayed values and presets.

Windows requires the MSVC toolchain, Visual Studio C++ Build Tools and Windows SDK. DirectWrite static axes require the IDWriteTextLayout4 runtime probe to succeed; unsupported runtimes omit the controls instead of pretending to apply axes. macOS uses the shared Apple font-kit/CoreText implementation.

For headless comparison output:

just text --backend both --timings-only --iterations 3

Linux: inspection versus editing

just text --backend both

The native Cosmic panel displays Roboto Flex's axis ranges and feature tags but omits editable sliders and static-axis specimens. The Parley panel applies axes with the same controls used on macOS, Windows and Wasm. Keep these capabilities separate: successful inspection does not establish arbitrary-axis shaping support.

The repository validation workflow runs Cosmic fixture tests, the Parley library, a headless native-host construction/axis smoke check and the shared slider tests on Ubuntu. This is distinct from interactive Wayland/X11, IME and accessibility acceptance on a Linux desktop.

Lab tests

just lab-test

Backtraces are enabled automatically. To isolate slider interaction failures:

just lab-test live_axis -- --nocapture

These tests send actual pointer and keyboard events through GPUI's simulated test windows, checking endpoint values, focus/keyboard changes, drag release, presets, layout changes and restored defaults. Windows uses a real DirectWrite text system: its headless platform deliberately supplies a no-op text backend and is unsuitable for this test. The preview-capacity test is included in the full lab suite.

Validation scope

The Wasm inspector was exercised in the local in-app browser on November 5, 2026, using both WebGPU and WebGL2. Checks covered all four pointer sliders, Home/arrow keyboard changes (including slnt=-9.75), the Wide preset and Default reset. The pinned specimen's 32px advance changed from 337.05px at Default to 446.73px at Wide and returned to 337.05px on reset. Axis ranges, GSUB/GPOS feature tags and static/numeric-feature specimens rendered through the Wasm pipeline. These are browser checks on this host, not exhaustive browser/device coverage.

The native/Parley labs and lab unit tests were confirmed working on macOS and Windows on November 5, 2026. A subtle cross-platform slant appearance difference was observed in both panels, with matching endpoints, and accepted as non-blocking.

Implementation-host checks also passed 52 core text tests (including six private parser/query tests), nine Apple text-system tests, 36 Parley tests and ten text-lab tests. Core tests distinguish unsupported queries, absent tables, read failures and malformed metadata. Backend fixtures cover resolved-face inspection, signed axes, clamping, fallback, stable identity and layout/raster agreement. Five standalone DirectWrite fixture tests and the lab test targets cross-compile for MSVC; separate native execution of the standalone fixture suite is not recorded.

The CPU sweep and Metal allocation baselines are documented in preview policy. They do not establish globally bounded scrubbing, frame-safe eviction, exhaustive runtime compatibility, physical-display equivalence or complete IME acceptance.