The devel branch of Clay Board Style System now supports four more font-relative length units: ex, ch, rex, and rch.
For readability, I will refer to Clay Board Style System as CBSS below.
CBSS is a CSS-inspired primitive engine for native GUI toolkits written mainly in Nim. It does not use a DOM or WebView, and it is not intended to reproduce the entire browser platform.
This update began with a small-looking requirement:
decl("width", ex(20))
decl("min-width", ch(12))
Adding four unit tags was easy. Resolving them correctly was not.
Unlike px, these units depend on the selected font. Unlike em, they cannot be calculated from font-size alone. The style resolver needs information from the text engine, but the core layout and style layers should not depend on one concrete font library.
This devlog explains how commit
8629108
addresses that boundary.
The feature is currently on devel and is intended for the v0.4 development line. It is not part of the current v0.3.2 release.
What ex, ch, rex, and rch mean
The distinction between the units matters to the implementation:
| Unit | Reference used by CBSS |
|---|---|
ex |
x-height of the current element's selected font |
ch |
advance measure of the 0 glyph in the current selected font |
rex |
root element's ex value |
rch |
root element's ch value |
These definitions follow the font-relative units in the CSS Values and Units specification.
The specification also defines a 0.5em fallback when the x-height or the horizontal 0 advance cannot be determined.
CBSS is CSS-inspired rather than a conforming browser engine, and the current adapter has one implementation difference worth making explicit. The CSS specification defines font-relative lengths without shaping, while the current cosmic-text adapter obtains ch by shaping "0" with the resolved text
configuration. This article describes the CBSS contract as it exists in this commit, not a claim of complete CSS conformance.
That fallback is important for a native style engine. CBSS must still behave deterministically in headless tests, through its C ABI, or when an application has not installed a full text engine.
Why font-relative units cross subsystem boundaries
A normal absolute length can be resolved inside the style system:
20px -> 20 layout pixels
An ex value needs a longer path:
font declarations
-> selected font face and variation settings
-> x-height in font units
-> scale by the computed font size
-> resolve ex into layout pixels
ch also depends on the font selection, but its reference is the shaped advance of the 0 glyph.
CBSS already has a replaceable TextEngine abstraction. Text shaping is provided by a cosmic-text adapter today, while layout and style resolution remain independent of cosmic-text. Importing the cosmic-text bridge directly into the property resolver would have broken that boundary.
The new implementation therefore introduces a small callback contract instead:
type
FontUnitMetrics = object
version: uint32
xHeight: float32
zeroAdvance: float32
FontUnitMetricsResolver = proc(
style: ComputedTextStyle
): FontUnitMetrics {.closure.}
The style resolver describes the resolved font selection. The installed text adapter answers with only the two measurements required for these units. Neither side needs to own the other subsystem.
Why the metrics contract is versioned
FontUnitMetrics carries a contract version even though it currently contains
only two values.
That gives CBSS an explicit compatibility boundary. A future text adapter may be built separately from the style engine, especially when accessed through a native language boundary. Returning structurally similar data is not enough if the two sides disagree about its meaning.
The resolver rejects an incompatible version and validates the fields independently. A non-finite or non-positive x-height does not invalidate a usable zero advance, and vice versa. Each invalid field falls back to 0.5em.
The same fallback is used when no provider is installed:
let halfEm = fontSize * 0.5
FontUnitMetrics(
version: fontUnitMetricsContractVersion,
xHeight: halfEm,
zeroAdvance: halfEm
)
This keeps the default debug engine, headless style tests, and the C ABI deterministic without pretending that the fallback is a measured font metric.
Resolution order is the difficult part
The style resolver cannot ask for current font metrics before it knows the current font. At the same time, a declaration such as this refers to font metrics:
decl("font-size", ex(2))
The implementation resolves a node in stages:
- Partition
font-size,line-height, otherfont-*descriptors, and the remaining declarations. - Resolve
font-sizewith the parent font metrics available. - Resolve the descriptors needed to choose the current font.
- Ask the metrics provider for the current x-height and zero advance.
- Resolve
line-heightand the remaining properties with the appropriate current and root metrics. - Pass stable root metrics and current metrics to child resolution.
This avoids a cycle and follows the important CSS distinction: font-relative units used in font-* properties resolve from the parent metrics, while ex and ch used in line-height continue to use the element's own metrics.
Root variants need another invariant. rex and rch must continue to refer to the root text style while descendants and independently invalidated subtrees are resolved. CBSS stores the root x-height and root zero advance in the resolution environment rather than recalculating them from each descendant.
Measuring real metrics with cosmic-text
The default full text adapter crosses from Nim into a Rust bridge built around cosmic-text.
For ex, the bridge selects the configured font face, applies variable-font settings, reads the face's x-height through ttf-parser, and scales the result from font units to the computed font size. If the font does not provide a valid x-height, the bridge returns the half-em fallback.
For ch, it shapes the string "0" with the resolved text configuration and uses the resulting layout-run width as the zero advance. If shaping cannot produce a positive finite value, it also falls back to half an em.
The metrics are cached by the resolved font configuration. This matters because style resolution may ask the same question for many nodes. Reopening font data or shaping 0 for every declaration would turn a small relative-unit feature into repeated text-engine work.
The cache is deliberately bounded. In this commit, the text adapter retains at most 128 font-metric entries.
The public units remain typed
The Nim API adds explicit constructors:
ex(2)
ch(3)
rex(1)
rch(8)
Internally, the corresponding unit kinds were appended after the existing public ordinals. This preserves the numeric values of earlier unit tags.
The versioned C ABI follows the same append-only rule. Its ABI version moves from 0x00010007 to 0x00010008, and the four new unit constants are added without renumbering the existing ones.
The C ABI uses the deterministic fallback metrics because a concrete text engine remains an application-side adapter concern at that boundary. A C consumer can therefore use ex and ch, but it should not mistake the fallback for a measurement from a particular installed font.
What the tests protect
The commit adds a dedicated 213-line unit test file and expands the cosmic-text and C-consumer tests.
The test matrix covers:
- stable public unit ordinals;
-
0.5embehavior without a metrics provider; - metrics based on the current font family and size;
- stable
rexandrchvalues across descendants; -
exandchinsideline-height; - independent fallback for invalid provider fields;
- diagnostics when standalone resolution has no font context;
- reuse of root and parent metrics during subtree resolution;
- cosmic-text x-height and zero-advance reporting;
- agreement between the reported zero advance and measured
0width; - C ABI construction and layout using the fallback path.
This is more test surface than the four new enum values suggest, but most bugs in relative-unit handling come from resolution context rather than arithmetic.
The larger design lesson
CSS-inspired native UI is not mainly a matter of copying property names.
Familiar syntax carries assumptions about font selection, inheritance, resolution order, fallback, and root-relative state.
For CBSS, the useful design was not to make the style resolver understand cosmic-text. It was to define the smallest versioned question the resolver could ask a text engine:
Given this resolved text style,
what are its x-height and zero-glyph advance?
That keeps the style system replaceable, the text engine replaceable, and the headless fallback deterministic.
The result is a small author-facing feature built on an explicit subsystem boundary:
ui.box(uiStyle([
decl("font-size", px(18)),
decl("width", ch(24)),
decl("min-height", ex(3))
])):
ui.text("Font-relative native layout")
Repository:
puffball1567/clay-board-style-system
About cosmic-text
This implementation builds on cosmic-text, a pure Rust library for multiline text shaping, layout, rendering, font fallback, and editing.
cosmic-text was created by Jeremy Soller and is developed in the pop-os/cosmic-text repository with contributions from its open-source community. CBSS uses it through a Rust bridge for native text shaping and measurement. The font-relative unit work in this devlog would be much larger without that foundation.
Top comments (0)