Wrapped-line virtualization — completes #33's word-wrap step (stacked on #34/#35/#37)#40
Draft
andrewtheart wants to merge 4 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.
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.
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.
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
CanvasTextLayoutand 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/…FromRenderedIndexgain a virtualized branch,MoveCursorByVisualRowsnavigates 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 inDraw,UpdateCurrentLineTextLayout, the index mappers,MoveCursorByVisualRowsand the hit-test. Row countingnow shares the same estimate helpers so the metrics and the render agree.
Core/Renderer/SelectionRenderer.cs— guards selection (see limitation).Known limitations (documented, niche)
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.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.