// lesson: scrolling-and-damage
Scrolling and Damage
Open a 200,000-line log file in your editor. The window shows 40 lines. Any work proportional to 200,000 โ wrapping every line, painting every line, even iterating every line per frame โ is work you'll feel as lag. The discipline that keeps editors fast has one name: do O(viewport) work, not O(document) work. This lesson builds its two halves: knowing which rows are on screen (scrolling & virtualization), and knowing which pixels actually changed (damage tracking).
The scroll model
Scrolling is one integer: scroll_y, the document-space pixel row shown
at the top of the viewport. The content is row_count * line_height
pixels tall; the viewport shows viewport_h of them. Three small
functions define the whole system, and each has a classic off-by-one
lurking:
- Clamping. Valid scroll positions are
[0, max(0, content_h - viewport_h)]โ you can't scroll above the top, and the last line stops at the bottom of the window rather than scrolling up past it. (When content is shorter than the viewport, the only valid position is 0 โ thatmax(0, ...)is the line everyone forgets, and without it short files jitter.) - Visibility. The rows intersecting the viewport run from
scroll_y / line_height(integer division โ a partially visible top row counts) through the row containing the viewport's last pixel: `ceil((scroll_y- viewport_h) / line_height)`, capped at the row count. Only rows in this half-open range get wrapped (via the lesson-9 cache) and painted. This range is the virtualization: nothing outside it is ever touched.
- Reveal. When typing or arrow keys move the caret off screen, the view follows โ by the minimum amount. If the caret row's top is above the viewport, align tops; if its bottom is below, align bottoms; if it's visible, don't move at all (gratuitous scrolling on every keystroke is deeply disorienting). Compute bottom-alignment first, then top โ in the degenerate case of a viewport shorter than one line, the row's top must win, so the top correction is applied last.
Where does the lesson-4 copy_rect fit? When scroll_y changes by ฮ,
the surviving viewport_h - |ฮ| pixels are already rendered โ blit them
to their new position and only the revealed strip needs painting. The
strip is damage, which brings us to the second half.
Damage: repaint what changed, nothing else
Between two frames, almost nothing changes: a character appears, the
caret moves, a selection extends. The naive editor repaints the window
anyway โ and at 4K that's 8 million pixels of glyph blitting per
keystroke, most redrawing what was already there. The professional
pattern is a damage list (the same idea the X server exposes as
Expose rectangles, Win32 as the invalid region, Cocoa as
setNeedsDisplayInRect:): every state change registers the rectangle
it dirtied; at paint time, only pixels inside the accumulated damage get
recomputed, and only that region is pushed to the screen.
The interesting design question is what to do when damage rectangles pile
up. Keep every rect and you repaint overlapping areas repeatedly and blit
dozens of tiny regions; collapse everything to one bounding box and a
caret blink plus a status-bar update repaints the whole diagonal between
them. The workable middle ground โ used, in fancier region-algebra form,
by every windowing system โ is pairwise coalescing with a no-waste
rule: merge two rects when doing so costs nothing, i.e. when they
overlap, or when their union's area doesn't exceed the sum of theirs
(adjacent same-height rects merge into one clean strip; far-apart specks
stay separate). One subtlety your implementation must handle: merging two
rects can make the merged result newly mergeable with a third โ after
each merge, rescan until nothing combines. The list stabilizes fast in
practice (an editor's damage is a handful of text bands), and take() โ
return everything, clear the list โ is the paint loop's entire interface.
Half-open rects earn their keep here one more time: "adjacent" is exactly
a.x + a.w == b.x, no fudge terms, and the area arithmetic in the merge
rule is exact.
โบ The Visible Window
12 ptsImplement the scroll math. All heights are pixels; rows is the total
visual row count from layout.
content_height(rows, lh)โ total pixel height.clamp_scroll(scroll_y, viewport_h, lh, rows)โ nearest valid scroll position.visible_rows(scroll_y, viewport_h, lh, rows)โ the half-open range of row indices intersecting the viewport.scroll_yis already clamped; a non-positiveviewport_hor zerorowsyields an empty range. Partially visible rows count at both ends.scroll_to_reveal(scroll_y, viewport_h, lh, row)โ the minimally adjusted scroll position makingrowfully visible (top-aligned when the viewport is shorter than one line, per the lesson).
Log in to submit a solution and earn points.
โบ The Damage List
15 ptsImplement DamageList with no-waste coalescing. The starter includes the
lesson-2 geometry kit, solved.
add(r): ignore empty rects. Otherwise mergerwith any stored rect it's mergeable with โ mergeable means they overlap, or the union's area is no larger than the sum of their areas (compute areas in 64-bit:long long). Merging removes the partner, replacesrwith the union, and rescans, because the grown rect may now absorb others. When nothing merges, storer.empty(),count(): bookkeeping for the tests.bounds(): bounding box of everything stored (empty rect when empty) โ what you'd hand to a singlexcb_put_image.take(): return the list and clear it โ called once per paint.
Log in to submit a solution and earn points.