detached 3d55321

fix(input): compute held wheel value after direction reset, skip empty pre-v8 frames

commit
3d55321528a48f0b08accea22f366893ac409a9e
author
brice · · 2026-09-25 19:58 UTC
parent
6ea6bb9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +33 -19
M internal/adapters/wayland/axis_test.go +11 -4
@@ -10,11 +10,13 @@ import (
1010 // discrete step once 120 builds up; a direction change drops the rest.
1111 func TestWheelSteps(t *testing.T) {
1212 s := &Server{}
13- wheel := func(v, h int32) [2]int32 {
14- return s.wheelSteps(ports.PointerAxis{
13+ var values [2]float64
14+ wheel := func(v, h int32) (steps [2]int32) {
15+ steps, values = s.wheelSteps(ports.PointerAxis{
1516 Vertical: ports.ScrollAxis{Set: v != 0, V120: v, Value: float64(v) / 8},
1617 Horizontal: ports.ScrollAxis{Set: h != 0, V120: h},
1718 })
19+ return steps
1820 }
1921 for i, tc := range []struct {
2022 v, h int32
@@ -38,10 +40,15 @@ func TestWheelSteps(t *testing.T) {
3840 if s.wheelHeld[0] != 7.5 {
3941 t.Fatal("held", s.wheelHeld[0])
4042 }
41- if wheel(60, 0) != [2]int32{1, 0} || s.wheelHeld[0] != 0 {
43+ if wheel(60, 0) != [2]int32{1, 0} || s.wheelHeld[0] != 0 || values[0] != 15 {
4244 t.Fatal("held after step", s.wheelHeld[0])
4345 }
44- if s.wheelSteps(ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, V120: 240}}) != [2]int32{} {
46+ // A direction change drops what was held the other way.
47+ wheel(60, 0)
48+ if wheel(-180, 0) != [2]int32{-1, 0} || values[0] != -22.5 {
49+ t.Fatal("after reversal", values)
50+ }
51+ if steps, _ := s.wheelSteps(ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, V120: 240}}); steps != [2]int32{} {
4552 t.Fatal("finger scroll made discrete steps")
4653 }
4754 }
@@ -10,11 +10,13 @@ import (
10 // discrete step once 120 builds up; a direction change drops the rest.10 // discrete step once 120 builds up; a direction change drops the rest.
11 func TestWheelSteps(t *testing.T) {11 func TestWheelSteps(t *testing.T) {
12 s := &Server{}12 s := &Server{}
13- wheel := func(v, h int32) [2]int32 {13+ var values [2]float64
14- return s.wheelSteps(ports.PointerAxis{14+ wheel := func(v, h int32) (steps [2]int32) {
15+ steps, values = s.wheelSteps(ports.PointerAxis{
15 Vertical: ports.ScrollAxis{Set: v != 0, V120: v, Value: float64(v) / 8},16 Vertical: ports.ScrollAxis{Set: v != 0, V120: v, Value: float64(v) / 8},
16 Horizontal: ports.ScrollAxis{Set: h != 0, V120: h},17 Horizontal: ports.ScrollAxis{Set: h != 0, V120: h},
17 })18 })
19+ return steps
18 }20 }
19 for i, tc := range []struct {21 for i, tc := range []struct {
20 v, h int3222 v, h int32
@@ -38,10 +40,15 @@ func TestWheelSteps(t *testing.T) {
38 if s.wheelHeld[0] != 7.5 {40 if s.wheelHeld[0] != 7.5 {
39 t.Fatal("held", s.wheelHeld[0])41 t.Fatal("held", s.wheelHeld[0])
40 }42 }
41- if wheel(60, 0) != [2]int32{1, 0} || s.wheelHeld[0] != 0 {43+ if wheel(60, 0) != [2]int32{1, 0} || s.wheelHeld[0] != 0 || values[0] != 15 {
42 t.Fatal("held after step", s.wheelHeld[0])44 t.Fatal("held after step", s.wheelHeld[0])
43 }45 }
44- if s.wheelSteps(ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, V120: 240}}) != [2]int32{} {46+ // A direction change drops what was held the other way.
47+ wheel(60, 0)
48+ if wheel(-180, 0) != [2]int32{-1, 0} || values[0] != -22.5 {
49+ t.Fatal("after reversal", values)
50+ }
51+ if steps, _ := s.wheelSteps(ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, V120: 240}}); steps != [2]int32{} {
45 t.Fatal("finger scroll made discrete steps")52 t.Fatal("finger scroll made discrete steps")
46 }53 }
47 }54 }
M internal/adapters/wayland/server.go +22 -15
@@ -403,10 +403,9 @@ func (s *Server) apply(cmd ports.ClientCommand) {
403403 if c.ID != s.pointerFocus {
404404 return
405405 }
406- held := s.wheelHeld
407- steps := s.wheelSteps(c.Axis)
406+ steps, values := s.wheelSteps(c.Axis)
408407 for _, p := range s.windowPointers(s.windows[c.ID]) {
409- sendAxis(p, c.Axis, steps, held)
408+ sendAxis(p, c.Axis, steps, values)
410409 }
411410 case ports.SetKeymap:
412411 s.setKeymap(c)
@@ -621,10 +620,11 @@ func (s *Server) changeFocus(id ports.WindowID) {
621620 // wheelSteps adds wheel v120 to the rest per axis and returns the whole
622621 // detents for axis_discrete (seat before v8). High-resolution wheels send
623622 // fractions of 120; a direction change drops the rest, as wlroots does.
624-// wheelHeld keeps the axis value of the frames without a step.
625-func (s *Server) wheelSteps(a ports.PointerAxis) (steps [2]int32) {
623+// wheelHeld keeps the axis value of the frames without a step; values is
624+// what a pre-v8 client gets with a step: the held value plus this frame's.
625+func (s *Server) wheelSteps(a ports.PointerAxis) (steps [2]int32, values [2]float64) {
626626 if a.Source != ports.AxisWheel {
627- return steps
627+ return steps, values
628628 }
629629 for i, ax := range [2]ports.ScrollAxis{a.Vertical, a.Horizontal} {
630630 if !ax.Set || ax.V120 == 0 {
@@ -638,22 +638,31 @@ func (s *Server) wheelSteps(a ports.PointerAxis) (steps [2]int32) {
638638 steps[i] = s.wheelRest[i] / 120
639639 s.wheelRest[i] -= steps[i] * 120
640640 if steps[i] != 0 {
641- s.wheelHeld[i] = 0
641+ values[i], s.wheelHeld[i] = s.wheelHeld[i], 0
642642 }
643643 }
644- return steps
644+ return steps, values
645645 }
646646
647647 // sendAxis sends one scroll frame, each event gated by the pointer version.
648648 // steps are whole wheel detents for axis_discrete. A pre-v8 client gets a
649-// wheel frame only with a step, carrying the value held since the last one
650-// (held, before this frame), so it never counts smooth and discrete scroll.
651-func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, held [2]float64) {
649+// wheel axis only with a step, carrying values (all the value since the
650+// last step), so it never counts smooth and discrete scroll.
651+func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) {
652652 v := p.Version()
653+ axes := [2]ports.ScrollAxis{a.Vertical, a.Horizontal}
654+ for i := range axes {
655+ if v < 8 && a.Source == ports.AxisWheel && axes[i].V120 != 0 && steps[i] == 0 {
656+ axes[i].Set = false
657+ }
658+ }
659+ if !axes[0].Set && !axes[1].Set {
660+ return
661+ }
653662 if v >= 5 {
654663 p.SendAxisSource(uint32(a.Source))
655664 }
656- for axis, s := range [2]ports.ScrollAxis{a.Vertical, a.Horizontal} {
665+ for axis, s := range axes {
657666 if !s.Set {
658667 continue
659668 }
@@ -668,10 +677,8 @@ func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, held [2]f
668677 switch {
669678 case v >= 8:
670679 p.SendAxisValue120(uint32(axis), s.V120)
671- case steps[axis] == 0:
672- continue
673680 default:
674- value += held[axis]
681+ value = values[axis]
675682 if v >= 5 {
676683 p.SendAxisDiscrete(uint32(axis), steps[axis])
677684 }
@@ -403,10 +403,9 @@ func (s *Server) apply(cmd ports.ClientCommand) {
403 if c.ID != s.pointerFocus {403 if c.ID != s.pointerFocus {
404 return404 return
405 }405 }
406- held := s.wheelHeld406+ steps, values := s.wheelSteps(c.Axis)
407- steps := s.wheelSteps(c.Axis)
408 for _, p := range s.windowPointers(s.windows[c.ID]) {407 for _, p := range s.windowPointers(s.windows[c.ID]) {
409- sendAxis(p, c.Axis, steps, held)408+ sendAxis(p, c.Axis, steps, values)
410 }409 }
411 case ports.SetKeymap:410 case ports.SetKeymap:
412 s.setKeymap(c)411 s.setKeymap(c)
@@ -621,10 +620,11 @@ func (s *Server) changeFocus(id ports.WindowID) {
621 // wheelSteps adds wheel v120 to the rest per axis and returns the whole620 // wheelSteps adds wheel v120 to the rest per axis and returns the whole
622 // detents for axis_discrete (seat before v8). High-resolution wheels send621 // detents for axis_discrete (seat before v8). High-resolution wheels send
623 // fractions of 120; a direction change drops the rest, as wlroots does.622 // fractions of 120; a direction change drops the rest, as wlroots does.
624-// wheelHeld keeps the axis value of the frames without a step.623+// wheelHeld keeps the axis value of the frames without a step; values is
625-func (s *Server) wheelSteps(a ports.PointerAxis) (steps [2]int32) {624+// what a pre-v8 client gets with a step: the held value plus this frame's.
625+func (s *Server) wheelSteps(a ports.PointerAxis) (steps [2]int32, values [2]float64) {
626 if a.Source != ports.AxisWheel {626 if a.Source != ports.AxisWheel {
627- return steps627+ return steps, values
628 }628 }
629 for i, ax := range [2]ports.ScrollAxis{a.Vertical, a.Horizontal} {629 for i, ax := range [2]ports.ScrollAxis{a.Vertical, a.Horizontal} {
630 if !ax.Set || ax.V120 == 0 {630 if !ax.Set || ax.V120 == 0 {
@@ -638,22 +638,31 @@ func (s *Server) wheelSteps(a ports.PointerAxis) (steps [2]int32) {
638 steps[i] = s.wheelRest[i] / 120638 steps[i] = s.wheelRest[i] / 120
639 s.wheelRest[i] -= steps[i] * 120639 s.wheelRest[i] -= steps[i] * 120
640 if steps[i] != 0 {640 if steps[i] != 0 {
641- s.wheelHeld[i] = 0641+ values[i], s.wheelHeld[i] = s.wheelHeld[i], 0
642 }642 }
643 }643 }
644- return steps644+ return steps, values
645 }645 }
646 646
647 // sendAxis sends one scroll frame, each event gated by the pointer version.647 // sendAxis sends one scroll frame, each event gated by the pointer version.
648 // steps are whole wheel detents for axis_discrete. A pre-v8 client gets a648 // steps are whole wheel detents for axis_discrete. A pre-v8 client gets a
649-// wheel frame only with a step, carrying the value held since the last one649+// wheel axis only with a step, carrying values (all the value since the
650-// (held, before this frame), so it never counts smooth and discrete scroll.650+// last step), so it never counts smooth and discrete scroll.
651-func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, held [2]float64) {651+func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) {
652 v := p.Version()652 v := p.Version()
653+ axes := [2]ports.ScrollAxis{a.Vertical, a.Horizontal}
654+ for i := range axes {
655+ if v < 8 && a.Source == ports.AxisWheel && axes[i].V120 != 0 && steps[i] == 0 {
656+ axes[i].Set = false
657+ }
658+ }
659+ if !axes[0].Set && !axes[1].Set {
660+ return
661+ }
653 if v >= 5 {662 if v >= 5 {
654 p.SendAxisSource(uint32(a.Source))663 p.SendAxisSource(uint32(a.Source))
655 }664 }
656- for axis, s := range [2]ports.ScrollAxis{a.Vertical, a.Horizontal} {665+ for axis, s := range axes {
657 if !s.Set {666 if !s.Set {
658 continue667 continue
659 }668 }
@@ -668,10 +677,8 @@ func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, held [2]f
668 switch {677 switch {
669 case v >= 8:678 case v >= 8:
670 p.SendAxisValue120(uint32(axis), s.V120)679 p.SendAxisValue120(uint32(axis), s.V120)
671- case steps[axis] == 0:
672- continue
673 default:680 default:
674- value += held[axis]681+ value = values[axis]
675 if v >= 5 {682 if v >= 5 {
676 p.SendAxisDiscrete(uint32(axis), steps[axis])683 p.SendAxisDiscrete(uint32(axis), steps[axis])
677 }684 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close