Skip to content

markdown: a wrapped table keeps its height while the reader scrolls - #298

Merged
foxnne merged 1 commit into
mainfrom
markdown/wrapped-table-heights
Oct 10, 2026
Merged

foxnne merged 1 commit into
mainfrom
markdown/wrapped-table-heights

Conversation

@foxnne

@foxnne foxnne commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

What changes

A markdown table whose cells wrap over many lines no longer changes height as the reader scrolls into it. The scrollbar no longer twitches by a line when paragraphs scroll back into view, either. The table-heavy test sample is now a generated document whose tables actually wrap. Both integration tests that guard this failed on it before the fix, and the old frozen plan copy hid the bug.

Why it moved. Everything traces to one dvui behaviour: a text layout on its first frame wraps at an assumed 1000pt, not at the column, so it measures short. The preview's bookkeeping believed those measurements in four places:

  1. Table cells settled short. A row measured only on budgeted frames, with skipped frames between, loses its text layouts in every gap, so every draw is a first frame. Two such draws agree perfectly, and the cell settled at the short height. A table re-entering the draw set also overwrote settled cells with short first-frame sizes. On the synthetic sample's last table, the final row settled 59pt short: 1789 measured off screen, 1730 on screen. Now a first-frame cell measurement never counts as a draw and never replaces a real size. It is kept (provisional) only when there is nothing better.
  2. Rows lagged a frame behind their cells. The grid sizes a row from what its cells reported last frame. With (1) fixed, the cache stayed right but the grid did not, and a settled table measured short, cleanly, on the frame after a first-frame draw. Now a drawn cell with a settled size at this width is held to at least that height, as a culled cell already was.
  3. A pinned block was measured by its pin. The wrapper emits exactly the pin, so a .deferred table came out of deferral with its untrusted height filed as a clean measurement. Now any pinned block is measured by its unconstrained inner box, which only pinned-above-the-fold blocks used to be. That also makes it safe to pin every block with a believed height, not only tables. A paragraph scrolled back into view emitted a line short for a frame. That was the remaining 19.6pt in "rapid scrolling up and down".
  4. .measured meant two things. It meant both "clean at this width" and "laid out at a width that is gone". record must throw the second away for a contaminated measurement, so it threw the first away too. Stale heights are now their own state, .stale. invalidateForWidth also no longer turns a never-measured .estimated slot into a "measurement" of its zero placeholder.

The task this came from guessed at the table estimate for wrapped cells. Instrumenting the failing block showed the estimate was never involved: the table settled at 1789.1 from a clean off-screen measurement, and the on-screen layout was the one that was wrong.

The sample. tests/data/markdown_sample_tables.md is now generated by tests/data/markdown_sample_tables.py (seeded, so it reproduces exactly). It has one 48KB three-column table whose cells wrap over many lines, three smaller tables, and prose. After the tables come blocks that repeat word for word, so source hashes are not unique (the old sample had seven, which the traversal test relies on), and one long paragraph for bench-markdown's "prose" sample. Those come after the tables on purpose: content inserted above them shifted which scroll frames see the big table, and that alone hid the block-heights failure. The frozen plan copy at that path is gone. bench-markdown's "prose" sample now uses this document as well.

SDK impact

  • None

Verified

  • macOS: all five gates (zig build, zig build test, zig build test-integration, zig build check-web, zig build test-sdk-version), re-run after rebasing onto main past docs: plans move to the private fizzyedit/plans #296.
    • On the parent, the new sample fails both tests: block 29 changed height while scrolling: 1789.1 -> 1730.4, and a rapid-scroll total of 18967.6 against 19476.0. With the fix, all 17 markdown preview integration tests pass.
    • With the fix, the 4-column **Row N** variant of the sample also passes all 17 (reported failing with 2610.4 -> 2297.5 before; I did not re-run that on the parent). So does the old frozen plan copy.
    • New unit tests in block_heights.zig: a contaminated measurement does not replace a clean one at the same width, and a width change leaves a never-measured block on its estimate.
  • Windows:
  • Linux: CI's test-integration
  • Web: check-web compiles. Not run in a browser.

Not checked by eye in the running app. The evidence is the headless layout tests above.

Follow-ups

  • Every block with a settled or deferred height is now pinned while drawn. A real change still gets through, one frame late: the inner box disagrees, the entry drops to .measured, and the pin comes off. That is the lag pinned-above-the-fold blocks already had.

🤖 Generated with Claude Code

@foxnne
foxnne force-pushed the docs/plans-private branch from 5a25fb8 to f304d96 Compare October 9, 2026 19:44
Base automatically changed from docs/plans-private to main October 9, 2026 19:57
@foxnne
foxnne marked this pull request as ready for review October 10, 2026 01:07
@foxnne
foxnne enabled auto-merge (squash) October 10, 2026 01:08
dvui wraps a text layout on its first frame at an assumed 1000pt, so it
measures short, and the preview believed those measurements: table cells
settled short when budgeted draws were all first frames, rows lagged a frame
behind their cells, a pinned block was measured by its pin, and `.measured`
meant both "clean at this width" and "stale from a width change". Each is
fixed, every block with a believed height is now pinned, and the table sample
is a generated document whose cells wrap, on which both scroll tests fail
without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@foxnne
foxnne force-pushed the markdown/wrapped-table-heights branch from e552c7a to 005e864 Compare October 10, 2026 02:51
@foxnne
foxnne merged commit 8c012ee into main Oct 10, 2026
9 of 10 checks passed
@foxnne
foxnne deleted the markdown/wrapped-table-heights branch October 10, 2026 02:58
This was referenced Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant