@@ -429,6 +429,16 @@ func MouseUp(button action.MouseButton) error {
429429// different distance depending on which backend answered.
430430const scrollPixelsPerNotch = 30
431431
432+ // scrollNotches is how many wheel notches a scroll of delta pixels is: the
433+ // nearest whole notch, and never fewer than one so a step shorter than a notch
434+ // still moves. Truncating instead left a default 50 px step one notch short
435+ // of the two it rounds to, and a 59 px step no further than a 30 px one.
436+ // Every unanimated Linux path and the animated total share this, so the two
437+ // travel the same distance.
438+ func scrollNotches (delta int ) int {
439+ return max ((abs (delta )+ scrollPixelsPerNotch / 2 )/ scrollPixelsPerNotch , 1 )
440+ }
441+
432442// ScrollAtCursor scrolls at the cursor, presenting modifiers as held.
433443//
434444// Linux has no event-flags concept, so a modifier is a real key press around
@@ -533,9 +543,6 @@ func scrollAtCursorNow(deltaX, deltaY int, modifiers action.Modifiers) error {
533543 waitForWaylandModifierPress ()
534544 }
535545
536- // Scale factor: each uinput scroll event approximates ~1 line.
537- const scrollScale = scrollPixelsPerNotch
538-
539546 // maxBatchEvents caps the number of uinput events sent per
540547 // write/flush to avoid overflowing the kernel evdev buffer (~8192
541548 // bytes) or the Wayland socket buffer. Each batch is kept small so
@@ -551,10 +558,7 @@ func scrollAtCursorNow(deltaX, deltaY int, modifiers action.Modifiers) error {
551558 return 0
552559 }
553560
554- totalNotches := abs (delta ) / scrollScale
555- if totalNotches == 0 {
556- totalNotches = 1
557- }
561+ totalNotches := scrollNotches (delta )
558562
559563 remainingNotches := totalNotches
560564 batch := make ([]int , 0 , maxBatchEvents )
@@ -584,7 +588,9 @@ func scrollAtCursorNow(deltaX, deltaY int, modifiers action.Modifiers) error {
584588 }
585589 }
586590
587- return uinputScrollRemainder (delta , totalNotches , remainingNotches , scrollScale )
591+ return uinputScrollRemainder (
592+ delta , totalNotches , remainingNotches , scrollPixelsPerNotch ,
593+ )
588594 }
589595
590596 remainY := sendScaledScroll (uinputScrollAxisVertical , deltaY )
@@ -618,10 +624,12 @@ func scrollAtCursorNow(deltaX, deltaY int, modifiers action.Modifiers) error {
618624// send, in the caller's pixels, for the virtual pointer to finish.
619625//
620626// It is zero whenever every notch went out, even when the delta was not a
621- // whole number of notches: a 50 px step is one 30 px notch, and the 20 px
622- // left over is rounding, not a lost scroll. Handing it on used to add a
623- // second, opposite-signed notch to every press on Hyprland, which Chromium
624- // and Electron clients summed to nothing.
627+ // whole number of notches: a 50 px step is two 30 px notches, and the 10 px
628+ // left over is rounding, not a lost scroll. Handing it on used to add an
629+ // extra, opposite-signed notch to every press on Hyprland, which Chromium
630+ // and Electron clients summed to nothing. When some notches remain, the
631+ // pixels handed on round to exactly the unsent count, so the virtual pointer
632+ // finishes the same scroll rather than a re-rounded one.
625633func uinputScrollRemainder (delta , totalNotches , remainingNotches , scale int ) int {
626634 if remainingNotches == 0 {
627635 return 0
0 commit comments