Long-line horizontal virtualization — step 2 of #33 (stacked on #34)#35
Draft
andrewtheart wants to merge 2 commits into
Draft
Conversation
Introduce a small IScrollOffsetSource abstraction so the editor scroll position is expressed in pixels rather than legacy scrollbar units (SingleLineHeight / DefaultVerticalScrollSensitivity). ScrollManager routes every offset read/write through it; the ScrollBarOffsetSource adapter backs it with the existing two ScrollBar primitives and does the pixel<->scrollbar-unit conversion internally (ScrollOffsetMath). Behavior is unchanged. Foundation for word-wrap scrolling, long-line horizontal virtualization and diagonal 2-axis touchpad scrolling (FrozenAssassine#33). Adds ScrollOffsetMathTests.
On files with pathologically long lines (minified JS/CSS/JSON, JSONL logs), the renderer joined every visible line into one multi-megabyte string and laid it out on every frame, causing horizontal-scroll stutter. TextRenderer.Draw now lays out only a sliced window of each visible line when a visible line exceeds 50k chars; ordinary files keep the untouched render path. The uniform slice window is re-aligned to document coordinates via HorizontalOffset + a per-line prefix sum, and the caret, click hit-testing and selection map through GetRenderedCharacterIndexForDocumentCharacter / GetDocumentCharacterIndexFromRenderedIndex / GetRenderedLayoutIndexForDocument (all no-ops when inactive). The window/index arithmetic is extracted into a WinUI-free HorizontalSliceMath with unit tests. Step 2 of FrozenAssassine#33.
This was referenced Jul 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 2 of the editor scroll/render series (#33)
Builds on #34 (the pixel-based scroll seam). This adds horizontal virtualization for very long lines.
The problem
On files with pathologically long lines — minified JS/CSS/JSON, JSONL logs where each record is a single
50k–5M char line — the renderer joins every visible line into one string and lays out the whole thing on
every frame. That per-frame multi-megabyte string build +
CanvasTextLayoutis the actual cause ofhorizontal-scroll stutter (and, at the extreme, layout-size failures). The vertical line virtualization
already in
CalculateLinesToRenderdoesn't help, because the cost is per line width, not line count.The approach
When any visible line exceeds
HorizontalVirtualizationThreshold(50k chars),TextRenderer.Drawlays outonly a sliced window of each visible line — the viewport plus a generous scroll buffer, a few KB total —
instead of the full join. Ordinary files never hit this path (the max-visible-line-length check short-circuits),
so their render path is byte-for-byte unchanged.
The slice is a single uniform window
[HorizontalSliceStart, +HorizontalSliceLength)applied to every visibleline, so re-aligning it to document coordinates is one pixel offset (
HorizontalSlicePixelOffset, folded intothe existing
HorizontalOffset) plus a per-line prefix sum (_renderedLineSlicePrefix) for the multi-lineselection mapping. The window is reused while the viewport stays inside a buffered "safe zone", so small
scrolls don't re-slice.
Document ↔ rendered-index mapping is funneled through three small methods on
TextRenderer(
GetRenderedCharacterIndexForDocumentCharacter,GetDocumentCharacterIndexFromRenderedIndex,GetRenderedLayoutIndexForDocument) that the caret, click hit-testing and selection call. All are no-ops whenvirtualization is inactive.
Correctness surfaces covered
HorizontalOffset(now includes the slice offset).CursorRenderermaps the document column into the sliced current-line layout and shifts x.CursorHelperhit-tests against the slice-shifted margin, then maps back.SelectionRenderermaps both endpoints through the per-line prefix (the cumulativefull-line-length math would over-count, since rendered prior lines are sliced/shorter).
for the extreme-long-line case.
Purity & tests
The window arithmetic and the index mapping (where an off-by-one silently misplaces the caret/selection) are
extracted into a WinUI-free
HorizontalSliceMaththatTextRendererdelegates to, so it's unit-testablewithout a GPU canvas.
HorizontalSliceMathTestscovers the viewport→window computation, the reuse safe-zone,the per-line slice length, and the column↔rendered-index round-trip.
Known limitation
Whitespace-visualization glyphs are not slice-offset-corrected on virtualized lines (they compute x from the
layout directly). It's off by default and only visible on 50k+ char lines; happy to fold it in if you'd like.
Verification
TextControlBox(library) builds clean; the test project builds clean (x64). As with #34, happy to adjustnaming/scope before the next PR in the series.