Text Measurement & Font Metrics
Building a Web Browser Engine: Layout & Rendering
Chapter 6 · Text Measurement & Font Metrics
Chapter 5 built a real line-breaking algorithm and honestly flagged its own weak point: every character, at every font-size, measured as a flat 8px. This chapter replaces that placeholder with something a real browser would actually trust — and finds that the placeholder wasn't just imprecise, it was wrong in a way that would visibly break real pages.
A Real (Simplified) Per-Glyph Width Table, in Em Units
Defining widths as a fraction of the font's own size — "em" units, matching how real font metrics actually work — is exactly what makes correct font-size scaling fall out for free: multiply every character's own em-width by whatever font_size_px actually is, and the whole word scales proportionally.
Bug 1: Chapter 5's Placeholder Had No Way to Know Font-Size Ever Mattered
measure_word_naive('Welcome') — Chapter 5's own flat model — has no font_size_px parameter at all. It returns 56, unconditionally, whether that word is rendered as tiny fine print or a giant headline. The real model gives 'Welcome' a width of 61.12px at the default 16px body-text size, and exactly double, 122.24px, at 32px. That doubling isn't an arbitrary test case — Course 1's own published UA_STYLESHEET already contains Rule(['h1'], [Declaration('font-size', '2em'), ...]). A real <h1>'s own text is measurably almost twice as wide, per word, as the identical text set as plain body copy — and the old placeholder had no way to represent that at all.
Bug 2: Even at One Fixed Font-Size, a Flat Model Ignores Real Glyph Shape
measure_word_naive('iiiii') and measure_word_naive('mmmmm') both return 40 — five characters, flat 8px each, no distinction at all. The real model, at the same 16px: 'iiiii' comes out to 22.4px, 'mmmmm' to 62.4px — nearly three times wider, matching how dramatically different these two letters actually look in any real font.
The Real, Motivated Overflow: An H1's Own Text
A real <h1> containing "Welcome home now", available width 150px, actual font-size 32px (per the real UA rule above).
128, comfortably under the 150px budget. But measured correctly, at the real 32px this text is actually rendered at, that one line's true rendered width is 266.24px — a genuine 116px overflow the old model had no way to see, because it never had font-size as an input in the first place.
[['Welcome'], ['home', 'now']]. 'Welcome' alone already takes 122.24px at this size, so it correctly gets its own line before the 150px budget is exceeded. Both resulting lines' own real rendered widths — 122.24px and 135.04px — are independently confirmed within the 150px budget.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| Widths defined in em units, scaling with font-size | Course 1 Chapter 7's own inheritance model — font-size is one of the properties Chapter 7 established as inheritable, meaning a real font-size genuinely does propagate down through nested inline elements like <b>, exactly the value this chapter's own measure_word needs |
| Chapter 5's own line-breaking loop reused completely unchanged | Chapter 5's own explicit forward reference — "the exact same break_into_lines() algorithm this chapter built stays unchanged; only measure_word()'s own implementation gets replaced" — confirmed exactly true here, only the measurement function itself changed |
| A real 116px overflow on genuinely real UA-stylesheet content | Chapter 2's own overflow bug and Chapter 3's own overflow bug — a recurring theme across this entire layout course: a plausible simplification quietly produces boxes or lines that visually overflow their own intended space, caught only once real numbers are actually run through it |
Hands-On Exercises
Call char_width_px('#', 16.0) — a character with no explicit entry in CHAR_WIDTHS_EM. Confirm it falls back to DEFAULT_CHAR_WIDTH_EM rather than raising an error, and compute the exact pixel value it should return at 16px.
Compare char_width_px('a', 16.0) against char_width_px('A', 16.0). Confirm the uppercase letter measures wider than its own lowercase counterpart at the identical font-size, and explain why this matches real typography rather than being an arbitrary choice in the width table.
Compute measure_word('Welcome', 24.0) and compare it against measure_word('Welcome', 16.0). Confirm the relationship between the two is exactly what the em-based scaling model predicts, using a font-size that isn't a clean doubling of the 16px reference, and explain why this confirms the scaling is genuinely linear rather than a special case that only happens to work for 2x.
Chapter 6 Quick Reference
- Real per-glyph widths, in em units: each character's own width is a fraction of the font's own size — multiplying by
font_size_pxgives correct scaling for free - Real bug found and fixed: Chapter 5's own flat model had no font-size input at all — verified measuring a real
<h1>'s own 32px text identically to 16px body text, causing a genuine 116px line overflow once run through real line-breaking - A second, independent bug: even at a single fixed font-size, a flat per-character model can't distinguish a narrow glyph (
'i') from a wide one ('m') — fixed by the real per-character table - Verified: the scaling relationship is genuinely linear — doubling font-size exactly doubles measured width, and this holds at any font-size, not just a clean 2x
- Chapter 5's own line-breaking algorithm needed zero changes — only the measurement function feeding it was replaced
- Next chapter: Painting — turning a finished layout tree into an ordered list of paint commands, the real bridge to actual pixels