Skip to content

Wrapped-line virtualization — completes #33's word-wrap step (stacked on #34/#35/#37)#40

Draft
andrewtheart wants to merge 4 commits into
FrozenAssassine:masterfrom
andrewtheart:feat/wrapped-line-virtualization
Draft

Wrapped-line virtualization — completes #33's word-wrap step (stacked on #34/#35/#37)#40
andrewtheart wants to merge 4 commits into
FrozenAssassine:masterfrom
andrewtheart:feat/wrapped-line-virtualization

Conversation

@andrewtheart

Copy link
Copy Markdown
Contributor

Stacked on #34, #35, #37. Builds on the scroll seam (#34), horizontal virtualization (#35) and word
wrap (#37), so until they merge this PR's diff also includes their files. Please review that chain first —
especially the word-wrap foundation #37 — I'll rebase onto master as it lands. Draft for that reason.

Completes #33's word-wrap step: wrapped-line virtualization

The word-wrap PR (#37) deliberately left one gap: a single line so long it wraps to more rows than fit on
screen
. #37 falls back to estimating such a line's row count (so the metrics don't blow up), but it still
lays the whole line out to render it — a multi-megabyte minified/JSONL line in wrap mode would build a giant
CanvasTextLayout and freeze. This PR virtualizes the rendering of that line.

Approach

When the only visible line is a very-long wrapped line (≥ 100k chars), the renderer materializes just the
visible row slice of it — starting at the scrolled-to row, on the same estimated char-per-row grid the
metrics already use — instead of the full line. The caret, click hit-testing and arrow navigation map onto
that grid (GetRenderedCharacterIndexForDocumentCharacter / …FromRenderedIndex gain a virtualized branch,
MoveCursorByVisualRows navigates the grid arithmetically instead of laying out the line, and the caret/
hit-test treat the line as pinned to the viewport top).

Changes

  • Core/Renderer/TextRenderer.cs — virtualization state + ShouldVirtualizeWrappedLine /
    BuildVirtualizedWrappedLineRenderData / EstimateWrapped*; virtualized branches in Draw,
    UpdateCurrentLineTextLayout, the index mappers, MoveCursorByVisualRows and the hit-test. Row counting
    now shares the same estimate helpers so the metrics and the render agree.
  • Core/Renderer/SelectionRenderer.cs — guards selection (see limitation).

Known limitations (documented, niche)

  • Selection and search-highlighting inside a virtualized line are skipped — the layout is only a row
    slice, so document-space indices don't map onto it. Selecting/searching within a single multi-MB line is
    the only case affected; everything else is unchanged. Skipped rather than drawn wrong (and it avoids
    overrunning GetCharacterRegions). Happy to follow up if you want full support.
  • The row grid is an estimate (monospace assumption), so scroll-thumb proportions on such a line are
    approximate — same trade-off Word wrap (visual-row model) — step 3 of #33 (stacked on #34, #35) #37 already makes for row counting.

Behavior & verification

No effect unless word wrap is on and a visible line is ≥ 100k chars. Library + test project build clean
(x64). This is the most experimental piece of the series and, like #37, wants a run on a real display — happy
to iterate.

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.
Introduce word wrap on the previously no-wrap-only control. A document line can span multiple visual rows, so scrolling, caret, selection, click hit-testing and arrow navigation work in visual-row space via a WrapRowMetrics prefix-sum model (document line <-> visual row, with cheap incremental updates) and WrapGeometry pointer math, both pure and unit-tested. Excludes very-long-line wrapped-line virtualization (row count falls back to an estimate above 100k chars). Behavior is unchanged when WordWrap is false. Step 3 of FrozenAssassine#33.
…w slice)

Completes FrozenAssassine#33's word-wrap step: a single line so long it wraps to more rows than fit is now rendered as just its visible row slice on an estimated char-per-row grid, instead of laying out the whole multi-megabyte line (which would freeze). Caret, click and arrow navigation map onto the grid; the line renders pinned to the viewport top. Selection and search-highlighting inside a virtualized line are skipped (documented limitation). No effect unless word wrap is on and a visible line is >=100k chars.
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