Repository navigation
gpui: hold hover still while the wheel scrolls under a still pointer - #57
Merged
Merged
Conversation
sunli829
marked this pull request as ready for review
October 12, 2026 02:36
sunli829
force-pushed
the
fast-chat-scroll-cpu
branch
from
October 12, 2026 03:47
35cd970 to
7c61fc5
Compare
sunli829
force-pushed
the
fast-wheel-hover-freeze
branch
from
October 12, 2026 03:50
8d081df to
3f0b375
Compare
sunli829
added a commit
that referenced
this pull request
Oct 12, 2026
#56) Users of Allsum reported high CPU when dragging the scrollbar of a long conversation. Profiling Allsum (release build, a long "Greeting" session with charts, tables and markdown) showed the transcript list's scroll layer bought almost nothing: rows were rendered again on every frame, and the layer was demoted or kept off on most frames. Three fixes here, one commit each. **Stacked on #48.** The base branch `fast-robust-retention-on-main` is #48 merged into current `main` (conflict in `fast/tests/mod.rs` resolved), so this diff shows only these changes. ### 1. A list row's writes while it is drawn are its own Writes made while a row was laid out, prepainted or painted counted as someone else's. A chart filling its own path cache as it paints made its row (and every row like it) render again on every scrolled frame. Those writes are now the row's own, as a view's writes while it renders already are. The write generation is shared with the thread like the read logs, for the list to read without an app at hand. Test: `rows_writing_what_they_read_as_they_are_drawn_keep_the_layer` (fails without the fix: every shown row re-renders per scroll). ### 2. Replay a list's layer instead of compositing tiles A list's layer keeps its rows' paint operations and replays those reaching the viewport into the frame, moved by the scroll. No tiles are rasterized for lists, so: - rows painting paths, or drawing over earlier rows' paths, are replayed in order (no overlay, no keep-off); - a background that cannot be baked (translucent window) no longer matters; - a list's layer is not demoted: repainting costs what drawing without the layer costs. Overscan and the per-frame render budget are unchanged. `OverlayReach` (lists only) is removed. Tests that encoded tile, overlay or demotion behaviour for lists were rewritten to assert the new behaviour; every `matches_layers_off` comparison passes unchanged. `composites()` in the test helpers now also counts replayed frames. This is the change most worth discussing: it trades GPU-side tile compositing for CPU-side replay of display lists for lists. In Allsum it was the largest single win for scrollbar drag. ### 3. A list's scrollbar drag is a scroll `ListState::set_offset_from_scrollbar` bumped the list's state version, which counts content changes too, so every drag frame repainted the layer. It now notes the offset like other code-set offsets (`ScrollLog::take_offsets_set`, matched to the painted list by its state), which counts as a scroll. Without layers the version still moves. Test: `a_list_dragged_by_its_scrollbar_is_composited` (fails without the fix). ### Measured in Allsum (experimental build: these fixes, the hover freeze of the stacked PR and the app-side fixes; the zero-overscan variant of replay. Without hover freeze, wheel CPU was ~37 %; drag was unchanged) | | wheel CPU | drag CPU | |---|---|---| | main | 54 % | 36 % | | with fixes | ~22 % | ~19 % | ### Measured again with the code of these PRs Allsum release build against this branch stack (#56, #57 and the lingering-nodes fix of #55), GPUI Kit's draft fix and longbridge/ai-chat#438, same session, quiet machine: | | wheel CPU | drag CPU | |---|---|---| | main | 47.2 % | 32.4 % | | these PRs | 21.9 % | 19.7 % (~2.2 ms/frame) | Keeping the overscan unchanged (unlike the experimental build) lost nothing. The app-side fixes are in separate PRs (GPUI Kit, ai-chat). Hover freezing while the wheel scrolls is a separate draft stacked on this one. `script/check-upstream`: 0 violations. `cargo test -p gpui --features test-support --lib` (681 passed), `cargo test -p gpui_perf`, `cargo clippy -p gpui --features test-support --lib --tests` pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A wheel moves what is under the pointer without the pointer moving: every frame of the scroll hovered another row, rendering the row it left and the row it entered again, and a hover could start an animation (a card fading in, a toolbar showing) that kept the list off its layer. In a long chat transcript a frame scrolled by the wheel cost about what it costs without layers. For 100 ms after a wheel event, while the pointer stays where the event was, nothing is hovered now: the frame's hit test, and the one a list foretells for its rows, are empty. When the wheel stops the window is drawn again and hover follows the pointer; a pointer that moves ends it at once. The crate's own tests, which scroll under a still pointer to test hover as rows move, turn it off; one test turns it on.
sunli829
force-pushed
the
fast-wheel-hover-freeze
branch
from
October 12, 2026 04:37
3f0b375 to
d14b0a1
Compare
trancong12102
added a commit
to aislopware/gpui-fast
that referenced
this pull request
Oct 12, 2026
Hover held still while a wheel scrolls under a still pointer. Begins beside our fast::focus::begin_frame in retained::begin_frame; window.rs's allowlist removed budget rises by the two lines longbridge#57 replaces. Our scrollbar-drag and wheel rules (bb41023, e1a4746) are untouched: a drag moves the pointer, which ends the freeze, and sends no wheel event. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
trancong12102
added a commit
to aislopware/gpui-fast
that referenced
this pull request
Oct 12, 2026
Hover held still while the wheel scrolls (longbridge#57) cleared every hover, so the first frame of a scroll built again every view hovered around the list, a sidebar's panel among them, for a hover that had not changed. Its end refreshed the window, building every view again. Of what was hovered when the wheel began, what has not moved now stays hovered and nothing else is; the list's foretold hit test holds the same. The end of the freeze hit tests the pointer again and marks the window dirty, so only what hovers differently is built again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
huacnlee
pushed a commit
that referenced
this pull request
Oct 16, 2026
#57 holds hover styling still for 100 ms after a wheel event while the pointer stays put. It did this by emptying the window's hit test (`mouse_hit_test`). The cursor is resolved from that same hit test, so every frozen frame set the arrow over text, and the next wheel event restored the text cursor: the cursor flickered while scrolling. ## Change - The hit test is no longer emptied during the freeze; it stays the geometry under the pointer. `hover_freeze::begin_draw` is removed and `hover_freeze::hit_test` always returns the real hit test. - The freeze moves into hover itself: `HitboxId::is_hovered` (and `hovered_now`, its retained-view twin) returns `false` while `hover_freeze::frozen` is true. - Cursor resolution uses `is_hovered_ignoring_last_input`, which doesn't check the freeze, so the cursor follows the geometry under the pointer. `reset_cursor_style` is unchanged. - A list's foretold hit test still stays empty while frozen (`fast/layers/lists.rs`), so rows aren't rendered again for hover. ## Tests - New: `the_cursor_follows_the_text_under_a_still_pointer_while_hover_is_frozen`. The text cursor stays over text throughout the freeze, and the arrow comes back once the pointer leaves the text. - Updated: `hover_held_still_while_the_wheel_scrolls_renders_no_row_for_it` now asserts that the hit test is non-empty while no hitbox reports hovered. - Test helpers: `TestPlatform::active_cursor_style` and `Window::set_owns_pointer` (`#[cfg(test)]`). --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
Experimental, stacked on #56. Changes hover semantics for every app, so it is a separate PR for discussion.
A wheel moves what is under the pointer without the pointer moving: every frame of a wheel scroll hovered another row, rendering the row it left and the row it entered again. A hover could also start an animation (a hover card fading in, a toolbar showing) that kept the list off its layer. In Allsum's long transcript, wheel scrolling cost about what it costs without layers.
For 100 ms after a wheel event, while the pointer stays where the event was, nothing is hovered: the frame's hit test, and the one a list foretells for its rows, are empty. When the wheel stops the window is drawn again (a timer refreshes it) and hover follows the pointer; a pointer that moves ends it at once.
fast/layers/hover_freeze.rs: the state (per window, inWindowLayers),note_wheel(frominput::before_dispatch),begin_draw(fromretained::begin_frame),hit_test.window.rs:mouse_hit_testmadepub(crate); the post-prepaint hit test goes throughhover_freeze::hit_test.script/upstream-allowlistraises window.rs's added budget from 80 to 82 for these two lines.cfg(test);hover_held_still_while_the_wheel_scrolls_renders_no_row_for_itturns it on (and fails with it off).Measured in Allsum (experimental build, same binary, toggled at runtime): wheel CPU 37 % without, 21 % with; drag unchanged (~20 %).
Open questions:
script/check-upstream: 0 violations.cargo test -p gpui --features test-support --lib(681 passed), clippy pass.🤖 Generated with Claude Code