fix(input): hold wheel axis value until a full detent for pre-v8 clients
- commit
- 6ea6bb9c059aa64e50b798a188b4e6f90da32abf
- author
- brice · · 2026-09-25 19:56 UTC
- parent
- 07d7b79
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +36 -11
M internal/adapters/wayland/axis_test.go +10 -1
| @@ -12,7 +12,7 @@ func TestWheelSteps(t *testing.T) { | |||
| 12 | 12 | s := &Server{} | |
| 13 | 13 | wheel := func(v, h int32) [2]int32 { | |
| 14 | 14 | return s.wheelSteps(ports.PointerAxis{ | |
| 15 | - | Vertical: ports.ScrollAxis{Set: v != 0, V120: v}, | |
| 15 | + | Vertical: ports.ScrollAxis{Set: v != 0, V120: v, Value: float64(v) / 8}, | |
| 16 | 16 | Horizontal: ports.ScrollAxis{Set: h != 0, V120: h}, | |
| 17 | 17 | }) | |
| 18 | 18 | } | |
| @@ -32,6 +32,15 @@ func TestWheelSteps(t *testing.T) { | |||
| 32 | 32 | t.Fatalf("step %d: got %v, want %v", i, got, tc.want) | |
| 33 | 33 | } | |
| 34 | 34 | } | |
| 35 | + | // The held value sums the frames since the last step. | |
| 36 | + | *s = Server{} | |
| 37 | + | wheel(60, 0) | |
| 38 | + | if s.wheelHeld[0] != 7.5 { | |
| 39 | + | t.Fatal("held", s.wheelHeld[0]) | |
| 40 | + | } | |
| 41 | + | if wheel(60, 0) != [2]int32{1, 0} || s.wheelHeld[0] != 0 { | |
| 42 | + | t.Fatal("held after step", s.wheelHeld[0]) | |
| 43 | + | } | |
| 35 | 44 | if s.wheelSteps(ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, V120: 240}}) != [2]int32{} { | |
| 36 | 45 | t.Fatal("finger scroll made discrete steps") | |
| 37 | 46 | } | |
| @@ -12,7 +12,7 @@ func TestWheelSteps(t *testing.T) { | |||||
| 12 | s := &Server{} | 12 | s := &Server{} | ||
| 13 | wheel := func(v, h int32) [2]int32 { | 13 | wheel := func(v, h int32) [2]int32 { | ||
| 14 | return s.wheelSteps(ports.PointerAxis{ | 14 | return s.wheelSteps(ports.PointerAxis{ | ||
| 15 | - | Vertical: ports.ScrollAxis{Set: v != 0, V120: v}, | 15 | + | Vertical: ports.ScrollAxis{Set: v != 0, V120: v, Value: float64(v) / 8}, |
| 16 | Horizontal: ports.ScrollAxis{Set: h != 0, V120: h}, | 16 | Horizontal: ports.ScrollAxis{Set: h != 0, V120: h}, | ||
| 17 | }) | 17 | }) | ||
| 18 | } | 18 | } | ||
| @@ -32,6 +32,15 @@ func TestWheelSteps(t *testing.T) { | |||||
| 32 | t.Fatalf("step %d: got %v, want %v", i, got, tc.want) | 32 | t.Fatalf("step %d: got %v, want %v", i, got, tc.want) | ||
| 33 | } | 33 | } | ||
| 34 | } | 34 | } | ||
| 35 | + | // The held value sums the frames since the last step. | |||
| 36 | + | *s = Server{} | |||
| 37 | + | wheel(60, 0) | |||
| 38 | + | if s.wheelHeld[0] != 7.5 { | |||
| 39 | + | t.Fatal("held", s.wheelHeld[0]) | |||
| 40 | + | } | |||
| 41 | + | if wheel(60, 0) != [2]int32{1, 0} || s.wheelHeld[0] != 0 { | |||
| 42 | + | t.Fatal("held after step", s.wheelHeld[0]) | |||
| 43 | + | } | |||
| 35 | if s.wheelSteps(ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, V120: 240}}) != [2]int32{} { | 44 | if s.wheelSteps(ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, V120: 240}}) != [2]int32{} { | ||
| 36 | t.Fatal("finger scroll made discrete steps") | 45 | t.Fatal("finger scroll made discrete steps") | ||
| 37 | } | 46 | } | ||
M internal/adapters/wayland/server.go +26 -10
| @@ -82,7 +82,8 @@ type Server struct { | |||
| 82 | 82 | nextPool uint64 | |
| 83 | 83 | focused ports.WindowID | |
| 84 | 84 | pointerFocus ports.WindowID | |
| 85 | - | wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis | |
| 85 | + | wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis | |
| 86 | + | wheelHeld [2]float64 // axis value held back with it for pre-v8 clients | |
| 86 | 87 | pointerX, pointerY float64 | |
| 87 | 88 | pointers map[server.Client][]*wayland.Pointer | |
| 88 | 89 | modState ports.ModState | |
| @@ -402,9 +403,10 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 402 | 403 | if c.ID != s.pointerFocus { | |
| 403 | 404 | return | |
| 404 | 405 | } | |
| 406 | + | held := s.wheelHeld | |
| 405 | 407 | steps := s.wheelSteps(c.Axis) | |
| 406 | 408 | for _, p := range s.windowPointers(s.windows[c.ID]) { | |
| 407 | - | sendAxis(p, c.Axis, steps) | |
| 409 | + | sendAxis(p, c.Axis, steps, held) | |
| 408 | 410 | } | |
| 409 | 411 | case ports.SetKeymap: | |
| 410 | 412 | s.setKeymap(c) | |
| @@ -619,6 +621,7 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||
| 619 | 621 | // wheelSteps adds wheel v120 to the rest per axis and returns the whole | |
| 620 | 622 | // detents for axis_discrete (seat before v8). High-resolution wheels send | |
| 621 | 623 | // fractions of 120; a direction change drops the rest, as wlroots does. | |
| 624 | + | // wheelHeld keeps the axis value of the frames without a step. | |
| 622 | 625 | func (s *Server) wheelSteps(a ports.PointerAxis) (steps [2]int32) { | |
| 623 | 626 | if a.Source != ports.AxisWheel { | |
| 624 | 627 | return steps | |
| @@ -628,18 +631,24 @@ func (s *Server) wheelSteps(a ports.PointerAxis) (steps [2]int32) { | |||
| 628 | 631 | continue | |
| 629 | 632 | } | |
| 630 | 633 | if (s.wheelRest[i] < 0) != (ax.V120 < 0) { | |
| 631 | - | s.wheelRest[i] = 0 | |
| 634 | + | s.wheelRest[i], s.wheelHeld[i] = 0, 0 | |
| 632 | 635 | } | |
| 633 | 636 | s.wheelRest[i] += ax.V120 | |
| 637 | + | s.wheelHeld[i] += ax.Value | |
| 634 | 638 | steps[i] = s.wheelRest[i] / 120 | |
| 635 | 639 | s.wheelRest[i] -= steps[i] * 120 | |
| 640 | + | if steps[i] != 0 { | |
| 641 | + | s.wheelHeld[i] = 0 | |
| 642 | + | } | |
| 636 | 643 | } | |
| 637 | 644 | return steps | |
| 638 | 645 | } | |
| 639 | 646 | ||
| 640 | 647 | // sendAxis sends one scroll frame, each event gated by the pointer version. | |
| 641 | - | // steps are whole wheel detents for axis_discrete. | |
| 642 | - | func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32) { | |
| 648 | + | // 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) { | |
| 643 | 652 | v := p.Version() | |
| 644 | 653 | if v >= 5 { | |
| 645 | 654 | p.SendAxisSource(uint32(a.Source)) | |
| @@ -654,14 +663,21 @@ func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32) { | |||
| 654 | 663 | } | |
| 655 | 664 | continue | |
| 656 | 665 | } | |
| 666 | + | value := s.Value | |
| 657 | 667 | if a.Source == ports.AxisWheel && s.V120 != 0 { | |
| 658 | - | if v >= 8 { | |
| 668 | + | switch { | |
| 669 | + | case v >= 8: | |
| 659 | 670 | p.SendAxisValue120(uint32(axis), s.V120) | |
| 660 | - | } else if v >= 5 && steps[axis] != 0 { | |
| 661 | - | p.SendAxisDiscrete(uint32(axis), steps[axis]) | |
| 671 | + | case steps[axis] == 0: | |
| 672 | + | continue | |
| 673 | + | default: | |
| 674 | + | value += held[axis] | |
| 675 | + | if v >= 5 { | |
| 676 | + | p.SendAxisDiscrete(uint32(axis), steps[axis]) | |
| 677 | + | } | |
| 662 | 678 | } | |
| 663 | 679 | } | |
| 664 | - | p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(s.Value)) | |
| 680 | + | p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(value)) | |
| 665 | 681 | } | |
| 666 | 682 | pointerFrame(p) | |
| 667 | 683 | } | |
| @@ -687,7 +703,7 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) { | |||
| 687 | 703 | if id == s.pointerFocus { | |
| 688 | 704 | return | |
| 689 | 705 | } | |
| 690 | - | s.wheelRest = [2]int32{} | |
| 706 | + | s.wheelRest, s.wheelHeld = [2]int32{}, [2]float64{} | |
| 691 | 707 | // The new client sets its own cursor on enter; until then, the arrow. | |
| 692 | 708 | s.cursorSurface = nil | |
| 693 | 709 | s.setCursor(ports.CursorChange{}) | |
| @@ -82,7 +82,8 @@ type Server struct { | |||||
| 82 | nextPool uint64 | 82 | nextPool uint64 | ||
| 83 | focused ports.WindowID | 83 | focused ports.WindowID | ||
| 84 | pointerFocus ports.WindowID | 84 | pointerFocus ports.WindowID | ||
| 85 | - | wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis | 85 | + | wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis |
| 86 | + | wheelHeld [2]float64 // axis value held back with it for pre-v8 clients | |||
| 86 | pointerX, pointerY float64 | 87 | pointerX, pointerY float64 | ||
| 87 | pointers map[server.Client][]*wayland.Pointer | 88 | pointers map[server.Client][]*wayland.Pointer | ||
| 88 | modState ports.ModState | 89 | modState ports.ModState | ||
| @@ -402,9 +403,10 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 402 | if c.ID != s.pointerFocus { | 403 | if c.ID != s.pointerFocus { | ||
| 403 | return | 404 | return | ||
| 404 | } | 405 | } | ||
| 406 | + | held := s.wheelHeld | |||
| 405 | steps := s.wheelSteps(c.Axis) | 407 | steps := s.wheelSteps(c.Axis) | ||
| 406 | for _, p := range s.windowPointers(s.windows[c.ID]) { | 408 | for _, p := range s.windowPointers(s.windows[c.ID]) { | ||
| 407 | - | sendAxis(p, c.Axis, steps) | 409 | + | sendAxis(p, c.Axis, steps, held) |
| 408 | } | 410 | } | ||
| 409 | case ports.SetKeymap: | 411 | case ports.SetKeymap: | ||
| 410 | s.setKeymap(c) | 412 | s.setKeymap(c) | ||
| @@ -619,6 +621,7 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||||
| 619 | // wheelSteps adds wheel v120 to the rest per axis and returns the whole | 621 | // wheelSteps adds wheel v120 to the rest per axis and returns the whole | ||
| 620 | // detents for axis_discrete (seat before v8). High-resolution wheels send | 622 | // detents for axis_discrete (seat before v8). High-resolution wheels send | ||
| 621 | // fractions of 120; a direction change drops the rest, as wlroots does. | 623 | // fractions of 120; a direction change drops the rest, as wlroots does. | ||
| 624 | + | // wheelHeld keeps the axis value of the frames without a step. | |||
| 622 | func (s *Server) wheelSteps(a ports.PointerAxis) (steps [2]int32) { | 625 | func (s *Server) wheelSteps(a ports.PointerAxis) (steps [2]int32) { | ||
| 623 | if a.Source != ports.AxisWheel { | 626 | if a.Source != ports.AxisWheel { | ||
| 624 | return steps | 627 | return steps | ||
| @@ -628,18 +631,24 @@ func (s *Server) wheelSteps(a ports.PointerAxis) (steps [2]int32) { | |||||
| 628 | continue | 631 | continue | ||
| 629 | } | 632 | } | ||
| 630 | if (s.wheelRest[i] < 0) != (ax.V120 < 0) { | 633 | if (s.wheelRest[i] < 0) != (ax.V120 < 0) { | ||
| 631 | - | s.wheelRest[i] = 0 | 634 | + | s.wheelRest[i], s.wheelHeld[i] = 0, 0 |
| 632 | } | 635 | } | ||
| 633 | s.wheelRest[i] += ax.V120 | 636 | s.wheelRest[i] += ax.V120 | ||
| 637 | + | s.wheelHeld[i] += ax.Value | |||
| 634 | steps[i] = s.wheelRest[i] / 120 | 638 | steps[i] = s.wheelRest[i] / 120 | ||
| 635 | s.wheelRest[i] -= steps[i] * 120 | 639 | s.wheelRest[i] -= steps[i] * 120 | ||
| 640 | + | if steps[i] != 0 { | |||
| 641 | + | s.wheelHeld[i] = 0 | |||
| 642 | + | } | |||
| 636 | } | 643 | } | ||
| 637 | return steps | 644 | return steps | ||
| 638 | } | 645 | } | ||
| 639 | 646 | ||||
| 640 | // 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. | ||
| 641 | - | // steps are whole wheel detents for axis_discrete. | 648 | + | // steps are whole wheel detents for axis_discrete. A pre-v8 client gets a |
| 642 | - | func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32) { | 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) { | |||
| 643 | v := p.Version() | 652 | v := p.Version() | ||
| 644 | if v >= 5 { | 653 | if v >= 5 { | ||
| 645 | p.SendAxisSource(uint32(a.Source)) | 654 | p.SendAxisSource(uint32(a.Source)) | ||
| @@ -654,14 +663,21 @@ func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32) { | |||||
| 654 | } | 663 | } | ||
| 655 | continue | 664 | continue | ||
| 656 | } | 665 | } | ||
| 666 | + | value := s.Value | |||
| 657 | if a.Source == ports.AxisWheel && s.V120 != 0 { | 667 | if a.Source == ports.AxisWheel && s.V120 != 0 { | ||
| 658 | - | if v >= 8 { | 668 | + | switch { |
| 669 | + | case v >= 8: | |||
| 659 | p.SendAxisValue120(uint32(axis), s.V120) | 670 | p.SendAxisValue120(uint32(axis), s.V120) | ||
| 660 | - | } else if v >= 5 && steps[axis] != 0 { | 671 | + | case steps[axis] == 0: |
| 661 | - | p.SendAxisDiscrete(uint32(axis), steps[axis]) | 672 | + | continue |
| 673 | + | default: | |||
| 674 | + | value += held[axis] | |||
| 675 | + | if v >= 5 { | |||
| 676 | + | p.SendAxisDiscrete(uint32(axis), steps[axis]) | |||
| 677 | + | } | |||
| 662 | } | 678 | } | ||
| 663 | } | 679 | } | ||
| 664 | - | p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(s.Value)) | 680 | + | p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(value)) |
| 665 | } | 681 | } | ||
| 666 | pointerFrame(p) | 682 | pointerFrame(p) | ||
| 667 | } | 683 | } | ||
| @@ -687,7 +703,7 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) { | |||||
| 687 | if id == s.pointerFocus { | 703 | if id == s.pointerFocus { | ||
| 688 | return | 704 | return | ||
| 689 | } | 705 | } | ||
| 690 | - | s.wheelRest = [2]int32{} | 706 | + | s.wheelRest, s.wheelHeld = [2]int32{}, [2]float64{} |
| 691 | // The new client sets its own cursor on enter; until then, the arrow. | 707 | // The new client sets its own cursor on enter; until then, the arrow. | ||
| 692 | s.cursorSurface = nil | 708 | s.cursorSurface = nil | ||
| 693 | s.setCursor(ports.CursorChange{}) | 709 | s.setCursor(ports.CursorChange{}) | ||