Line stats
Before you render text, you often want to ask questions about it: How many lines will this become? Will it overflow the container I've allotted? What is the tightest column width that would still hold every word? Line stats answer these without painting anything.
Type below. The readout updates on every keystroke — line count, the widest wrapped line, the average line, and the natural width (the tightest column that would still avoid any break). Drag the target width to see which stat crosses which threshold.
Mechanism
Stats without materialization is what walkLineRanges(prepared, width, onLine) exists for. It runs the same line-break loop as layoutWithLines, but it never builds the line strings — for each line it just calls your callback with { width, start, end }. The heavy lifting (segmenting the text, measuring every word, applying the break rules) already happened inside prepare. The walk itself is arithmetic over cached numbers.
To measure the natural width — the tightest column that avoids any line break — we run walkLineRanges a second time with an enormous width, so nothing breaks, and take the width of the single returned line. This is the web's long-missing “shrink-wrap”: the exact minimum column into which this paragraph fits without any word hyphenating or wrapping.
Both calls happen synchronously inside the input handler. On a long paragraph this is still well under a millisecond; on a typical message it's closer to a hundred microseconds. Cheap enough that you can run it every keystroke without debouncing.
Application
Live-stat panes are the first obvious use, but most of the leverage is elsewhere:
- A writer's pane that shows line count and overflow threshold as they type, without a layout pass.
- A caption field that turns a warning light on when the current string will wrap past its two-line budget.
- An auto-fit layout that binary-searches the font size until
lineCountlands on a target —walkLineRangesreturns the count alone, so each trial is cheap. - A button label that decides whether to show the full name or the abbreviation based on whether natural width exceeds the button's fixed width.
Every one of these would cost a hidden DOM node and a forced reflow a year ago. Now it costs the length of this sentence in microseconds.
“A foolish consistency is the hobgoblin of little minds, adored by little statesmen and philosophers and divines. With consistency a great soul has simply nothing to do. He may as well concern himself with his shadow on the wall.”
Ralph Waldo Emerson, Self-Reliance (1841)Direct Claude
walkLineRanges in the input handler; count callbacks
“will this overflow?”
→
compare the walk's max line width to the container
“shrink-wrap the column”
→
walk at a huge width; take the resulting single line's width
“auto-fit the font size”
→
binary-search font size; each trial a cheap walkLineRanges
import { prepareWithSegments, walkLineRanges } from '@chenglou/pretext';
function computeStats(text, font, targetWidth) {
const prepared = prepareWithSegments(text, font);
// Walk once at the target width to get count + widths.
let lineCount = 0;
let maxW = 0;
let totalW = 0;
walkLineRanges(prepared, targetWidth, line => {
lineCount++;
totalW += line.width;
if (line.width > maxW) maxW = line.width;
});
// Walk again with a huge width; nothing breaks; that's the natural width.
let naturalWidth = 0;
walkLineRanges(prepared, 1_000_000, line => {
if (line.width > naturalWidth) naturalWidth = line.width;
});
return {
lineCount,
maxLineWidth: maxW,
avgLineWidth: lineCount ? totalW / lineCount : 0,
naturalWidth,
overflows: maxW > targetWidth,
};
}