Repository navigation
fix(explorer): the sweep box keeps up with the pointer (#332) - #333
Merged
Merged
Conversation
Owner, 2026-10-08: "fast movements make it fall behind the cursor, while it should stay at the cursor position perfectly the whole time, file explorer's highlight does it perfectly". The box was React state set on every pointermove, so the list (and the tree) rendered it after each frame was painted. useSweep now only mounts the band (sweeping, bandRef); the pointer handler writes its place straight onto the element from geometry measured at the press and after a scroll, with no layout read per move. Hit-testing and onChange run at most once a frame in the tick, from the latest position, and once more on the release, so the marks that stand are the box's. Measured by the new sweepLag e2e (300 rows, CDP moves of 25-40 px, one and four per frame, then an auto-scroll hold), moving corner against the pointer in the frame about to be painted: - before: list and tree, every move one event behind (max 53.8 px, mean about 23 px); auto-scroll every frame off (max 430 px list, 203 px tree) - after: 0 px in every frame, list and tree, auto-scroll included Patch bump to 0.99.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
…frame (#332) Review of #332: - measure again when the sweep starts (a wheel between the press and the threshold move left the box off by that scroll) and at the release, before the last marks; - a ResizeObserver on the scroller and its contents, and window resize, so rows added or removed and pane resizes are seen mid-sweep; - the band mounts through flushSync in the move that starts the sweep: a state update from a native listener painted that frame with no box (MEASURED: sweepLag now counts such a frame as late and failed 1 of 120 in both lists without the fix); - a comment on the compressed-scroll frame of disagreement in BrowseList. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
The box trailed unevenly: renders of the list mid-sweep, a scroll round-trip through App that undid newer scrolls, a box inside the scroller that re-rastered the rows, and fractional edges. As Explorer's UIMarqueeSelector does it: - marks during a sweep are the hook's (data-sweep-mark on the rows in view, drawn by the stylesheets), committed to React once on release - the Explorer's list draws from its own scroll and tells App on scrollend, at a release and before a press or key - the box is a clipped overlay beside the scroller, anchored in list coordinates, snapped to whole device pixels, one device pixel of edge - one position source per pointermove, no per-frame layout read sweepLag now asserts device-pixel edges, a still anchor and no list render while the box moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
…helpers (#332) A wheel turn after Escape, with the button still held, came to rest while the list carried data-sweeping, so scrollend told App nothing and the place waited for the next press or key. bandBox, Band and sameHits had no callers left after the box moved out of React state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
…ged nothing visible (#332) The owner, zoomed in, saw no difference with the DOM marks, the scroll round-trip fix and the pixel-snapped overlay, so the smaller code stays. CLAUDE.md records what was tried and that the rest of the trail is Chromium's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t
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.
What the owner said
Cause
The sweep box was React state. Every pointermove set it, and React rendered the list after the frame had been painted, so each frame showed the box where the pointer had been one move before. While the list auto-scrolled, the box was placed from the rendered
scrollTop, so every scroll frame was off as well.Fix
useSweeponly mounts the band (sweeping,bandRef). The pointer handler writes its place straight onto the element, from geometry measured at the press, at the sweep's start, after a scroll or a resize (ResizeObserver, window resize) and at the release. Nothing reads layout per move.flushSyncin the move that starts the sweep. A plain state update from a native listener could paint that first frame with no box.Before and after (
sweepLage2e, 300 rows, CDP moves of 25-40 px)Handler cost after: mean 0.07-0.08 ms, max 1.8 ms per pointermove.
Tests
sweepLage2e: it samples the box against the pointer in every painted frame through a fast walk, a burst of four moves a frame, and an auto-scroll hold, in the tree and in the Explorer, and asserts 0 frames more than 1 px off. A frame past the threshold with no box showing counts as late. Without theflushSyncmount that check fails 1 of 120 in both lists. It also checks that a burst released at once leaves the same marks as a slow sweep.marquee,marqueeQuiet,marqueeEdge,drag,dragLabele2e pass.Version 0.99.1 (patch).
Closes #332
🤖 Generated with Claude Code
https://claude.ai/code/session_01FHHaWKR4M5QtW7Wecyuk4t