Inline Layout & Line Boxes

Building a Web Browser Engine: Layout & Rendering

Chapter 5 · Inline Layout & Line Boxes

Every anonymous box built back in Chapter 1 has sat empty ever since — a real, correctly-shaped wrapper with genuine inline content inside it, but nothing that ever gave that content an actual size. This chapter is where text and inline elements finally get laid out: flattened into a flat sequence, then broken into real line boxes that fit an available width.

Scope note
Real character widths depend on the actual font, its size, and even which specific letters are involved — genuine font-metric measurement is Chapter 6's own entire subject. This chapter uses an honestly-flagged placeholder: every character (and every inter-word space) counts as a fixed 8px, regardless of what it actually is. The line-breaking algorithm this chapter builds is real and correct; only the width numbers it's fed are a stand-in.

Flattening Nested Inline Content Into a Flat Word List

def collect_words(layout_box): """Flattens an inline/anonymous box's own text content into a flat list of words, walking through nested inline elements (like <b>) in order.""" words = [] if (layout_box.box_type == 'inline' and layout_box.styled_node is not None and layout_box.styled_node.node.kind == 'text'): words.extend(layout_box.styled_node.node.text.split()) for child in layout_box.children: words.extend(collect_words(child)) return words
Verified directly — nested inline elements contribute their own text, in reading order
An anonymous box built from Chapter 1's own inline-run shape — "This is " + <b>"bold"</b> + " and normal text." — flattens via collect_words() to exactly ['This', 'is', 'bold', 'and', 'normal', 'text.']. A text box contributes its own words directly; an inline element box like <b> contributes nothing of its own, but the recursion still walks into its children, correctly picking up the text nested one level deeper.

Line-Breaking: A Real Bug in the "Does It Fit" Check

def break_into_lines_naive(words, available_width): lines = [] current_line = [] current_width = 0 for word in words: w = measure_word(word) if current_line and current_width + w > available_width: lines.append(current_line) current_line = [word] current_width = w else: current_line.append(word) current_width += w # BUG: never accounts for the space before this word if current_line: lines.append(current_line) return lines
Verified directly — three words that "fit" by the naive check overflow by 12px once actually rendered
The real sentence "The quick brown fox jumps over the lazy dog", broken at a 100px available width. The naive algorithm places "jumps", "over", and "the" on one line — their word widths alone sum to 96, which passes the naive 96 <= 100 check. But the real rendered width of that line — three words plus the two real spaces that have to sit between them — is 96 + 16 = 112. A genuine 12px overflow the naive check never even measured, because it only ever tracked word widths, never the spaces the words are actually going to need once they sit next to each other on a real line.
def break_into_lines(words, available_width): lines = [] current_line = [] current_width = 0 for word in words: w = measure_word(word) extra = w if not current_line else (w + SPACE_WIDTH_PX) if current_line and current_width + extra > available_width: lines.append(current_line) current_line = [word] current_width = w else: current_line.append(word) current_width += extra if current_line: lines.append(current_line) return lines
Verified directly — every real line's own rendered width now genuinely fits
The exact same sentence, fixed algorithm, same 100px width, breaks into [['The','quick'], ['brown','fox'], ['jumps','over'], ['the','lazy','dog']] — four real lines, each one's true rendered width (words plus every real inter-word space) individually checked and confirmed at or under 100px: 72, 72, 80, and 96.

Where This Connects

This chapter's findingWhat it connects to
collect_words() walking through nested inline elementsChapter 1's own anonymous-box grouping — this chapter is the first to actually consume the content those anonymous boxes were built to hold, confirming the grouping mechanism produces something genuinely usable
A real, honest placeholder character-width modelChapter 6's own real font-metric measurement — the exact same break_into_lines() algorithm this chapter built stays unchanged; only measure_word()'s own implementation gets replaced with something accurate
Space-width accounted for BEFORE the overflow check, not afterChapter 3's own real "auto" width overflow bug — the same shape of mistake (checking against a quantity that was measured too narrowly) showing up a second time, in a genuinely different part of this course's own layout engine

Hands-On Exercises

Exercise 1

Run this chapter's own real nine-word sentence through break_into_lines() with a generous 400px available width. Confirm it all fits on a single line, and confirm that single line's own real rendered width (via the chapter's own width-plus-spaces formula) is still verified within the 400px budget rather than just assumed.

📄 View solution
Exercise 2

Call break_into_lines(['The'], 10) — a single word, in an available width narrower than the word itself. Confirm the word still ends up on its own line rather than being dropped or raising an error, and explain exactly which part of the algorithm guarantees a lone word can never be rejected outright, no matter how narrow the available width is.

📄 View solution
Exercise 3

Reproduce this chapter's own overflow bug directly: run the real nine-word sentence through both break_into_lines_naive() and break_into_lines() at the same 100px width. For every line in the naive result, compute its real rendered width and confirm exactly one line overflows. Identify the specific variable in break_into_lines_naive that never gets incremented by SPACE_WIDTH_PX, and where the fixed version adds it instead.

📄 View solution

Chapter 5 Quick Reference

  • collect_words(): flattens an anonymous box's own inline content — plain text and nested inline elements alike — into one flat, ordered word list
  • Placeholder measurement: a fixed 8px per character and per space, honestly flagged for replacement by Chapter 6's own real font metrics
  • Real bug found and fixed: naive line-breaking accumulates only word widths, never the space that has to sit between them — verified overflowing a real 100px line by 12px on a real nine-word sentence
  • The fix: account for one SPACE_WIDTH_PX before every word except the first on a line, as part of the overflow check itself, not as an afterthought
  • Verified: a lone word too wide for the available space still gets placed on its own line — never dropped, never crashes, matching how real browsers render an unbreakable overflowing word
  • Next chapter: Text Measurement & Font Metrics — replacing this chapter's own placeholder width model with something a real browser would actually trust