Skip to content

fix(virtual-list): report only ranges the list actually draws - #56

Merged
guyverino merged 1 commit into
masterfrom
fix/virtual-list-visible-range
Aug 28, 2026
Merged

fix(virtual-list): report only ranges the list actually draws#56
guyverino merged 1 commit into
masterfrom
fix/virtual-list-visible-range

Conversation

@guyverino

Copy link
Copy Markdown
Collaborator

Problem

MoonVirtualList::on_visible_range invoked its observer from the closure handed to uniform_list. That closure is also used to MEASURE one row: measure_item renders item_to_measure_index..+1 from request_layout and again from prepaint, before the real range exists. The observer therefore saw a phantom 0..1 twice per frame ahead of every real range.

An observer that evicts state for rows outside the reported range — keyboard focus, an open popup — then evicted everything but the measured row on every frame. Downstream, MoonTerminal's Connections tab blurred the core-name field one frame after the click, so a core could not be renamed at all. The other consumer, the chart stack, only survived because its handler is an idempotent assignment that the real range repaired later in the same frame.

Change

UniformList gains its own on_visible_range channel, invoked once per prepaint with the range that goes to the item renderer (flipped indices included, while decorations keep receiving unflipped positions). MoonVirtualList forwards to it; its public signature is unchanged.

Two halves to the reporting rule, and both are needed:

  • a list holding no rows reports 0..0;
  • a list that holds rows but shows none of them — zero height, mid-collapse, not yet sized — stays silent, so a squeezed list is never mistaken for an empty one.

An emptiness test alone reports 12..13 for a collapsed list whose offset is not a whole multiple of a row; a height test alone silences a merely padded list whose row is plainly visible, because Taffy floors the element at its own padding.

The builder now documents what the runtime does not promise: reporting is per prepaint rather than per frame (a cached ancestor skips it, a retried Window::transact pass repeats it), cx.notify and Window::refresh are dropped mid-draw while Window::blur still takes effect, and a scroll requested from the observer lands on the next frame.

Verification

  • tools\run-component-guardrails.ps1 → PASS (437 component tests, 7 gallery, audit/API/mirror baselines unmoved).
  • cargo test -p moon-gpui → 180 passed. Not part of CI, run locally because this touches the runtime.
  • Six new gpui::test cases plus one in moon-gpui, held by a new audit contract virtual_list.visible_range_reporting. Each was proven by mutation: wiring the observer back into the renderer yields [0..1, 0..1, 0..10]; reporting visible_range instead of the rendered range makes a flipped list claim 40..50; the old padded-box guard yields [] instead of [11..12]; dropping the height half yields [12..13] for a collapsed list; deleting a test drops the contract to Fail.
  • MoonTerminal built against this branch through a local [patch], cargo test --workspace green, and the Connections rename confirmed working by hand.

docs/MOON_PATCH_QUEUE.md records the patch so a re-sync of the GPUI extraction restores both halves of the rule.

`MoonVirtualList::on_visible_range` called its observer from the closure it
hands to `uniform_list`. That closure has a second job: `measure_item` renders
`item_to_measure_index..+1` to obtain a row height, from `request_layout` and
again from `prepaint`, before the real range exists. An observer wired through
it therefore saw a phantom `0..1` twice per frame ahead of every real range.

An observer that evicts state for rows outside the range it is told about —
keyboard focus, an open popup — then evicted everything but the measured row
on every frame. Downstream, MoonTerminal's Connections tab blurred the core
name field one frame after the click, so a core could not be renamed at all.

`UniformList` gains its own `on_visible_range` channel, invoked once per
prepaint with the range that goes to the item renderer, flipped indices
included. The reporting rule has two halves the tests pin down: a list holding
no rows reports `0..0`, while a list that holds rows but shows none of them —
zero height, mid-collapse, not yet sized — stays silent, so a squeezed list is
never mistaken for an empty one. Both conditions are needed: an emptiness test
alone reports `12..13` for a collapsed list at an offset that is not a whole
multiple of a row, and a height test alone silences a merely padded list whose
row is plainly visible, because Taffy floors the element at its own padding.

The builder documents what the runtime does and does not promise: it reports
per prepaint rather than per frame, `cx.notify` and `Window::refresh` are
dropped mid-draw while `Window::blur` still takes effect, and a scroll the
observer requests lands on the next frame.

Covered by `virtual_list.visible_range_reporting`, a new audit contract naming
six behavioural tests, and by one test in `moon-gpui` itself. `MoonVirtualList`
keeps its public signature, so no baseline moves.
@guyverino
guyverino merged commit 850de06 into master Aug 28, 2026
2 checks passed
@guyverino
guyverino deleted the fix/virtual-list-visible-range branch August 28, 2026 07:49
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