Skip to content

Fix stale rows when scrolling a restricted region (CoreGraphics renderer)#582

Open
tavvet wants to merge 1 commit into
migueldeicaza:mainfrom
tavvet:fix-restricted-region-scroll-repaint
Open

Fix stale rows when scrolling a restricted region (CoreGraphics renderer)#582
tavvet wants to merge 1 commit into
migueldeicaza:mainfrom
tavvet:fix-restricted-region-scroll-repaint

Conversation

@tavvet

@tavvet tavvet commented Jun 24, 2026

Copy link
Copy Markdown

Problem

Full-screen ncurses apps that keep a fixed header and/or footer — nano, and vim/less/htop in some layouts — set a restricted scroll region (DECSTBM, e.g. rows [1, 43] of a 46-row screen). While scrolling such a region, rows outside the region keep stale pixels, and a "ghost" line lingers on the very last row while paging.

Reproducible on the CoreGraphics renderer (Metal disabled): open nano on a file with lines wider than the window and hold PageDown/PageUp — stale rows and a bottom ghost appear.

Root cause

scroll(), reverseIndex(), cmdScrollUp() and cmdScrollDown() flag only [scrollTop, scrollBottom] for refresh via updateRange. For a restricted region (scrollTop != 0 or scrollBottom != rows-1) or the alternate buffer:

  • the rows outside the region (and the whole-viewport shift scroll() makes via yBase) are never repainted, and
  • the last row is only repainted when getUpdateRange() returns rowEnd == rows-1, which triggers updateDisplay's bottom-edge "remaining bits" branch — a restricted region never produces that rowEnd.

So those rows keep stale backing-store pixels on the CG path.

Fix

Add refreshScrolledRegion(top:bottom:): when the region is restricted or we're on the alternate buffer, repaint the whole viewport (endLine == rows-1); a real full-screen scrollback scroll keeps the cheap region-only path (its scroll blit handles the bottom edge). Call it from the four scroll primitives. The whole-viewport repaint is safe on the Metal path too (it just widens the dirty range).

Testing

Logged updateDisplay's (rowStart, rowEnd): a restricted-region scroll now produces rowEnd == rows-1 (bottom-edge branch) instead of rowEnd == scrollBottom. Verified visually in nano/vim/less — no stale rows or bottom ghost while paging. The package builds cleanly.

Full-screen apps with a fixed header/footer use a restricted scroll region.
The scroll primitives flagged only [scrollTop, scrollBottom] for refresh, so
rows outside the region — and the last row, which needs rowEnd == rows-1 to hit
updateDisplay's bottom-edge "remaining bits" branch — kept stale pixels on the
CoreGraphics renderer: right-side tails on long lines and a ghost line at the
bottom while paging in nano.

Add refreshScrolledRegion(top:bottom:): repaint the whole viewport for a
restricted region or the alternate buffer; a full-screen scrollback scroll keeps
the cheap region-only path. Used by scroll(), reverseIndex(), cmdScrollUp() and
cmdScrollDown().
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