Skip to content

fast: Keep the cursor following the pointer while hover is frozen - #72

Merged
huacnlee merged 2 commits into
longbridge:mainfrom
zzfn:cursor-during-hover-freeze
Oct 16, 2026
Merged

huacnlee merged 2 commits into
longbridge:mainfrom
zzfn:cursor-during-hover-freeze

Conversation

@zzfn

@zzfn zzfn commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

#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)]).

Clearing the frame hit test during a wheel scroll also cleared the
cursor, so the platform cursor flipped between the text cursor and the
arrow on every frame. Hover styling still reports nothing hovered. The
hit test stays the geometry under the pointer.

The wheel test reads the platform cursor: it stays the text cursor
while hover styling is held still, and becomes the arrow once the
pointer leaves the text.
@huacnlee huacnlee changed the title cursor: Follow the pointer while hover styling is frozen fast: Keep the cursor following the pointer while hover is frozen Oct 16, 2026

@huacnlee huacnlee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving the freeze from the hit test into hover is the right direction: the cursor goes through is_hovered_ignoring_last_input, so it follows the geometry. A few things need to change before this can merge.

1. A click within 100 ms of a wheel event, without moving the pointer, is dropped

While frozen, HitboxId::is_hovered now returns false for every hitbox. frozen() only checks the pointer position, and a mouse down at the same position doesn't change it. The div mouse down/up/click listeners all require hitbox.is_hovered(window) (elements/div.rs:135, :152, :168, …), so that click never reaches its handler.

Before this PR clicks still worked, because dispatch_mouse_event recomputes mouse_hit_test before dispatching (window.rs:6006). Two possible fixes:

  • end the freeze on any non-wheel pointer input (mouse down/up), or
  • apply the freeze only to hover styling while drawing, not to event dispatch.

Please add a test: wheel, then mouse down/up at the same position, and assert that the click handler runs.

2. script/check-upstream fails (2 violations)

  • platform/test/platform.rs:428: TestPlatform::active_cursor_style is a new method in an upstream file.
  • window.rs:3018: Window::set_owns_pointer (plus its doc comment) is a new method in an upstream file. window.rs is also over the budget (+112 > 99, −37 > 36).

Methods on upstream types belong in an impl block under fast/ (e.g. a #[cfg(test)] impl in fast/tests/ or the topic module). The upstream side should only bump visibility where needed (active_cursor, active, hovered → pub(crate)).

3. Nit: the freeze check is duplicated

HitboxId::is_hovered (window.rs) and hovered_now (fast/retained.rs) now both repeat last_input_was_keyboard() || hover_freeze::frozen(window). A single fast/ helper called from both places would keep the window.rs hook to one call.

While hover was held still after a wheel event, `HitboxId::is_hovered`
answered false outside drawing too. A mouse listener asks it before it
runs, so a click under a still pointer within 100 ms of the wheel never
reached its handler. Holding is now for drawing only: event dispatch
reads the geometry under the pointer, as it did before the hit test was
left intact.

`hover_freeze::hides_hover` is the one check, for `is_hovered` and for
what a retained subtree records of hover, so `window.rs` keeps a single
hook. `hover_freeze::hit_test` had become upstream's own line, so that
hook is back to upstream's. The test-only `TestPlatform::active_cursor_style`
and `Window::set_owns_pointer` move to `fast/tests/support.rs`; upstream
files only make `active_cursor`, `active` and `hovered` `pub(crate)`.

A new test clicks right after the wheel at the same position and expects
the handler to run. The two wheel tests no longer ask `is_hovered` between
frames, where it now reports the geometry; that hover styling is held
still is shown by no row being rendered for its hover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zzfn

zzfn commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, addressed in bdfeda0:

  1. Hover is now held only while drawing, so dispatch sees the geometry and the click lands. Added a_click_under_a_still_pointer_right_after_the_wheel_reaches_its_handler (fails on the previous commit).
  2. Test helpers moved to fast/tests/support.rs; upstream only gets the pub(crate) bumps. hover_freeze::hit_test had become upstream's own line, so I restored it, which keeps window.rs at +99/−36.
  3. One helper, hover_freeze::hides_hover, used by both is_hovered and hovered_now.

The two wheel tests no longer assert !is_hovered between frames, since outside drawing it now reports the geometry. Held hover styling is still covered by no row being rendered for its hover.

@huacnlee
huacnlee merged commit fa133a1 into longbridge:main Oct 16, 2026
4 checks passed
trancong12102 added a commit to aislopware/gpui-fast that referenced this pull request Oct 17, 2026
longbridge#73 keeps a list row's render- and layout-time element states while its
layer keeps it. longbridge#71 moves hover-freeze and profiling onto scheduler's
clock for WASM. longbridge#72 stops emptying the hit test while hover is frozen, so
the cursor follows the pointer and a click right after the wheel reaches
its handler.

The fork's hover freeze keeps hovered what has not moved since the wheel
began (the panel around a list), which upstream's drops. With longbridge#72 the
two meet in `hides_hover`: the hit test stays real, and while a frame is
drawn with hover held still only what hover holds where it was reads as
hovered. Both sides' tests are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
trancong12102 added a commit to aislopware/gpui-fast that referenced this pull request Oct 17, 2026
…r as drawn in retained checks

longbridge#72 moved the freeze into hover itself, so every `is_hovered` while a
frame was drawn asked `frozen`, which reads the clock. A wheel frame of a
scrolled child view cost 18 % more instructions (0.82 M to 0.97 M,
gpui_perf --headless). The frame now takes whether hover is held as it
begins, with what it holds, and hover reads that.

`hovered_now`, what a retained subtree records of a hover and the check
that the recorded hovers still hold, now reads hover as a frame draws it
whatever the phase: that check runs as a frame begins, before its
phases, where the freeze would otherwise read as a changed hover.

The child-view wheel frame is now 2.6 % over the fork before longbridge#72
(0.821 M to 0.843 M instructions), what the cursor following the
pointer costs; the median scenario moves 0.1 %.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants