simvx.core.text.measure

Text measurement: widths taken from a font’s own glyph advances.

Widget auto-sizing, the test harness, and the renderer must agree on how wide a string is, or a button is drawn narrower than the text inside it. They agree by measuring the same way: walk the glyph advances of the font the text will be drawn with, adding kerning between neighbours, and take the widest line.

The font used for measurement is the one the engine bundles, loaded on first use so that importing the engine never reads a TTF. A renderer that draws with a different font, or that has metrics but no font file at all (the browser runtime, where the glyph advances arrive pre-baked in the atlas), calls

func:

set_metrics_font to say so, and measurement follows it. Because that can happen after a scene has already sized itself, every change notifies the listeners registered with :func:add_metrics_listener, which is how content-sized widgets re-fit themselves instead of keeping a width measured against a font that is not the one being drawn.

A renderer that draws characters its primary font lacks by borrowing them from other fonts passes that lookup as fallback, so measurement borrows from the same fonts rather than assuming a tofu box. Nothing is borrowed for a character defined to leave no mark of its own, on either side: the renderer will not draw one from a face that happens to carry it, so measuring one that way would reserve room the text is never drawn in.

Kerning is looked up between neighbours the font draws as glyphs, including a blank one such as the space. A character no font can draw, which is reserved the room the renderer draws its missing-glyph box in, breaks the pair on both sides: a box is not a glyph of the face, so no pair in the face’s kerning table describes it, and the layout that draws it does not kern either side of one. Measuring it any other way would size a widget to a width that is never drawn. A character that no face draws and that occupies no space either is the opposite case: it has no room and no ink to come between its neighbours, so it is stepped over entirely and the pair reaches across it, which again is what the layout does.

Text that sits on a character grid, a terminal or a code view or a tabular readout, says so by passing the pitch of that grid as cell_width. A character nothing can draw then occupies exactly one cell, whichever form the renderer puts in it, so every column after it stays where the grid puts it. Nothing about text off a grid changes.

Measurement never raises. If no font can be loaded the width falls back to a fixed per-character advance, which is approximate but keeps a game laying out text on a machine whose font data is gone.

Module Contents

Functions

set_metrics_font

Measure text against font from now on, instead of the bundled default.

add_metrics_listener

Call callback whenever the font or the cached measurements change.

invalidate_text_metrics

Drop cached measurements and tell listeners the widths have moved.

metrics_font

The font text is measured against, loading the bundled one if needed.

measure_text_width

Width in pixels of text drawn at scale, from the font’s own advances.

text_prefix_widths

Width of every prefix of text at scale, from none of it to all of it.

reset

Forget the registered font and every cached measurement.

text_minimum_width

func:

measure_text_width rounded up to a whole pixel.

Data

API

simvx.core.text.measure.log

‘getLogger(…)’

simvx.core.text.measure.__all__

[‘add_metrics_listener’, ‘invalidate_text_metrics’, ‘measure_text_width’, ‘metrics_font’, ‘set_metri…

simvx.core.text.measure.LOGICAL_EM

16.0

simvx.core.text.measure.DEFAULT_METRICS_PX

48.0

simvx.core.text.measure.FALLBACK_ADVANCE

0.6

simvx.core.text.measure.WIDTH_CACHE_LIMIT

8192

simvx.core.text.measure.WIDTH_CACHE_EVICT

None

simvx.core.text.measure.set_metrics_font(font: collections.abc.Callable[[str], Any] | None, /, fallback=None) None[source]

Measure text against font from now on, instead of the bundled default.

font answers has_glyph(char), get_glyph(char).advance_x and get_kerning(left, right) in pixels, and reports the pixel size those advances are in as size: :class:~simvx.core.text.Font and the browser runtime’s pre-baked metrics both qualify. Passing None restores the bundled font.

fallback maps a character font has no glyph for to the font that will be drawn in its place, or to None for a tofu box. A renderer that borrows CJK or icon glyphs from other faces passes it, so that a label holding such text is measured as wide as it is drawn instead of as a row of tofu.

A renderer calls this with the font it actually draws with, so that a widget which sizes itself to fit its text really does fit it. Content-sized widgets that measured against the previous font are re-fitted, so this may be called at any point, including after a scene has been built.

simvx.core.text.measure.add_metrics_listener(callback: collections.abc.Callable[[str], None], /) None[source]

Call callback whenever the font or the cached measurements change.

Widths already computed are stale from that moment on, so whatever caches them (content-sized widgets, a layout) re-measures here. The callback is passed the characters whose metrics moved, or an empty string when the whole font changed and every measurement is suspect. Listeners are held for the life of the process and so belong to modules, not to instances.

simvx.core.text.measure.invalidate_text_metrics(chars: str = '', /) None[source]

Drop cached measurements and tell listeners the widths have moved.

Called when the font changes, and by a renderer that has just gained glyphs it previously had to approximate. In the second case the renderer passes the characters it gained, so that only text using them is re-measured: a browser rasterising a page of glyphs a few at a time invalidates on many consecutive frames, and re-fitting every widget on each of them costs more than the rasterising does.

Cached string widths are dropped whichever case it is. Working out which cached strings contain a character costs several milliseconds at the sizes this cache reaches, which is more than simply measuring them again.

simvx.core.text.measure.metrics_font()[source]

The font text is measured against, loading the bundled one if needed.

None when no font could be loaded at all, in which case measurement approximates with a fixed per-character advance.

simvx.core.text.measure.measure_text_width(text: str, scale: float = 1.0, *, cell_width: float = 0.0) float[source]

Width in pixels of text drawn at scale, from the font’s own advances.

scale is the renderer’s text scale: 1.0 draws at a font size of 16. Multi-line strings return their widest line, newlines advancing nothing and breaking the kerning pair, which is the contract the renderer’s own measurement follows.

A character no font can supply a glyph for advances by

Func:

~simvx.core.text.missing_glyph_advance, which is the room the renderer draws its box in. That room depends on scale, because large enough text writes the codepoint out across several boxes where small text draws one, so such a string is not simply proportional to scale the way ordinary text is. The walk itself is still cached per string: what the size decides is only whether the extra room the written-out form needs is added to it.

cell_width is the pitch of the character grid the text sits on, in the pixels this returns, and is how a caller that has a grid says so. On a grid a boxed character is reserved exactly one cell, so that the columns after it stay where the grid puts them; text that is not on a grid leaves it at zero and measures exactly as before.

simvx.core.text.measure.text_prefix_widths(text: str, scale: float = 1.0, *, cell_width: float = 0.0) list[float][source]

Width of every prefix of text at scale, from none of it to all of it.

Entry i is the width of the first i characters, so the list is one longer than the text and starts at 0.0. Hit-testing a click against a line of text needs all of these, and asking :func:measure_text_width for each prefix separately would both re-walk the line for every column and leave a cache entry per column behind. This walks the line once and caches nothing.

text is a single line: a newline is measured as advancing nothing, the same as anywhere else, rather than starting the widths over.

cell_width is the pitch of the character grid the text sits on, and means what it does in :func:measure_text_width. Hit-testing takes it from the same place the layout does, so a click lands on the column the glyph under it was drawn in even when a boxed character earlier on the line took a cell of its own rather than the room a box asks for in proportional text.

simvx.core.text.measure.reset() None[source]

Forget the registered font and every cached measurement.

simvx.core.text.measure.text_minimum_width(text: str, scale: float = 1.0, *, cell_width: float = 0.0) float[source]
Func:

measure_text_width rounded up to a whole pixel.

Widgets size themselves to this. Layout containers work in whole pixels, so a fractional minimum can be rounded down to just under what the text needs and clip its last column; asking for the next whole pixel cannot be.