detached 1ee32a2

merge: mouse (scroll wheel and all pointer buttons)

commit
1ee32a2c1be7a423c8810c81c2b4ea1fd68d9a45
author
brice · · 2026-09-25 19:59 UTC
merge of
9bc7abf + 3d55321 · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +254 -11
M internal/adapters/libinput/libinput.go +28 -0
@@ -27,6 +27,9 @@ const (
2727 evPointerMotion = 400
2828 evPointerAbs = 401
2929 evPointerButton = 402
30+ evScrollWheel = 404
31+ evScrollFinger = 405
32+ evScrollCont = 406
3033 )
3134
3235 var (
@@ -60,6 +63,9 @@ var (
6063 pointerAbsY func(pev uintptr, height uint32) float64
6164 pointerButton func(pev uintptr) uint32
6265 pointerBtnState func(pev uintptr) int32
66+ pointerHasAxis func(pev uintptr, axis uint32) int32
67+ scrollValue func(pev uintptr, axis uint32) float64
68+ scrollV120 func(pev uintptr, axis uint32) float64
6369 iface [2]uintptr
6470 active ports.Seat
6571 )
@@ -109,6 +115,9 @@ func load() error {
109115 reg(&pointerAbsY, "event_pointer_get_absolute_y_transformed")
110116 reg(&pointerButton, "event_pointer_get_button")
111117 reg(&pointerBtnState, "event_pointer_get_button_state")
118+ reg(&pointerHasAxis, "event_pointer_has_axis")
119+ reg(&scrollValue, "event_pointer_get_scroll_value")
120+ reg(&scrollV120, "event_pointer_get_scroll_value_v120")
112121 iface[0] = purego.NewCallback(func(path *byte, _, _ uintptr) uintptr {
113122 fd, err := active.OpenDevice(goString(path))
114123 if err != nil {
@@ -286,6 +295,25 @@ func translate(ev uintptr, opts Options, p *pointer) (ports.InputEvent, error) {
286295 b, pressed := pointerButton(pe), pointerBtnState(pe) == 1
287296 log.Debug().Uint32("button", b).Bool("pressed", pressed).Msg("button")
288297 return ports.PointerButton{Button: b, Pressed: pressed, TimeMsec: now}, nil
298+ case evScrollWheel, evScrollFinger, evScrollCont:
299+ pe := pointerEvent(ev)
300+ // The three scroll events follow each other as the sources do.
301+ source := ports.AxisSource(eventType(ev) - evScrollWheel)
302+ a := ports.PointerAxis{Source: source, TimeMsec: now}
303+ // libinput axis 0 is vertical, 1 horizontal, as in wl_pointer.
304+ for i, s := range []*ports.ScrollAxis{&a.Vertical, &a.Horizontal} {
305+ if pointerHasAxis(pe, uint32(i)) == 0 {
306+ continue
307+ }
308+ s.Set, s.Value = true, scrollValue(pe, uint32(i))
309+ if source == ports.AxisWheel {
310+ s.V120 = int32(scrollV120(pe, uint32(i)))
311+ } else {
312+ s.Stop = s.Value == 0
313+ }
314+ }
315+ log.Debug().Float64("v", a.Vertical.Value).Float64("h", a.Horizontal.Value).Int32("v120", a.Vertical.V120).Int32("hv120", a.Horizontal.V120).Uint8("source", uint8(a.Source)).Msg("scroll")
316+ return a, nil
289317 }
290318 return nil, nil
291319 }
@@ -27,6 +27,9 @@ const (
27 evPointerMotion = 40027 evPointerMotion = 400
28 evPointerAbs = 40128 evPointerAbs = 401
29 evPointerButton = 40229 evPointerButton = 402
30+ evScrollWheel = 404
31+ evScrollFinger = 405
32+ evScrollCont = 406
30 )33 )
31 34
32 var (35 var (
@@ -60,6 +63,9 @@ var (
60 pointerAbsY func(pev uintptr, height uint32) float6463 pointerAbsY func(pev uintptr, height uint32) float64
61 pointerButton func(pev uintptr) uint3264 pointerButton func(pev uintptr) uint32
62 pointerBtnState func(pev uintptr) int3265 pointerBtnState func(pev uintptr) int32
66+ pointerHasAxis func(pev uintptr, axis uint32) int32
67+ scrollValue func(pev uintptr, axis uint32) float64
68+ scrollV120 func(pev uintptr, axis uint32) float64
63 iface [2]uintptr69 iface [2]uintptr
64 active ports.Seat70 active ports.Seat
65 )71 )
@@ -109,6 +115,9 @@ func load() error {
109 reg(&pointerAbsY, "event_pointer_get_absolute_y_transformed")115 reg(&pointerAbsY, "event_pointer_get_absolute_y_transformed")
110 reg(&pointerButton, "event_pointer_get_button")116 reg(&pointerButton, "event_pointer_get_button")
111 reg(&pointerBtnState, "event_pointer_get_button_state")117 reg(&pointerBtnState, "event_pointer_get_button_state")
118+ reg(&pointerHasAxis, "event_pointer_has_axis")
119+ reg(&scrollValue, "event_pointer_get_scroll_value")
120+ reg(&scrollV120, "event_pointer_get_scroll_value_v120")
112 iface[0] = purego.NewCallback(func(path *byte, _, _ uintptr) uintptr {121 iface[0] = purego.NewCallback(func(path *byte, _, _ uintptr) uintptr {
113 fd, err := active.OpenDevice(goString(path))122 fd, err := active.OpenDevice(goString(path))
114 if err != nil {123 if err != nil {
@@ -286,6 +295,25 @@ func translate(ev uintptr, opts Options, p *pointer) (ports.InputEvent, error) {
286 b, pressed := pointerButton(pe), pointerBtnState(pe) == 1295 b, pressed := pointerButton(pe), pointerBtnState(pe) == 1
287 log.Debug().Uint32("button", b).Bool("pressed", pressed).Msg("button")296 log.Debug().Uint32("button", b).Bool("pressed", pressed).Msg("button")
288 return ports.PointerButton{Button: b, Pressed: pressed, TimeMsec: now}, nil297 return ports.PointerButton{Button: b, Pressed: pressed, TimeMsec: now}, nil
298+ case evScrollWheel, evScrollFinger, evScrollCont:
299+ pe := pointerEvent(ev)
300+ // The three scroll events follow each other as the sources do.
301+ source := ports.AxisSource(eventType(ev) - evScrollWheel)
302+ a := ports.PointerAxis{Source: source, TimeMsec: now}
303+ // libinput axis 0 is vertical, 1 horizontal, as in wl_pointer.
304+ for i, s := range []*ports.ScrollAxis{&a.Vertical, &a.Horizontal} {
305+ if pointerHasAxis(pe, uint32(i)) == 0 {
306+ continue
307+ }
308+ s.Set, s.Value = true, scrollValue(pe, uint32(i))
309+ if source == ports.AxisWheel {
310+ s.V120 = int32(scrollV120(pe, uint32(i)))
311+ } else {
312+ s.Stop = s.Value == 0
313+ }
314+ }
315+ log.Debug().Float64("v", a.Vertical.Value).Float64("h", a.Horizontal.Value).Int32("v120", a.Vertical.V120).Int32("hv120", a.Horizontal.V120).Uint8("source", uint8(a.Source)).Msg("scroll")
316+ return a, nil
289 }317 }
290 return nil, nil318 return nil, nil
291 }319 }
A internal/adapters/wayland/axis_test.go +54 -0
@@ -0,0 +1,54 @@
1+package wayland
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/nefertty/internal/ports"
7+)
8+
9+// High-resolution wheels send fractions of a detent: pre-v8 clients get a
10+// discrete step once 120 builds up; a direction change drops the rest.
11+func TestWheelSteps(t *testing.T) {
12+ s := &Server{}
13+ var values [2]float64
14+ wheel := func(v, h int32) (steps [2]int32) {
15+ steps, values = s.wheelSteps(ports.PointerAxis{
16+ Vertical: ports.ScrollAxis{Set: v != 0, V120: v, Value: float64(v) / 8},
17+ Horizontal: ports.ScrollAxis{Set: h != 0, V120: h},
18+ })
19+ return steps
20+ }
21+ for i, tc := range []struct {
22+ v, h int32
23+ want [2]int32
24+ }{
25+ {60, 0, [2]int32{}},
26+ {60, 0, [2]int32{1, 0}},
27+ {180, 0, [2]int32{1, 0}},
28+ {60, 0, [2]int32{1, 0}},
29+ {-60, 240, [2]int32{0, 2}},
30+ {-60, 0, [2]int32{-1, 0}},
31+ {30, -30, [2]int32{}},
32+ } {
33+ if got := wheel(tc.v, tc.h); got != tc.want {
34+ t.Fatalf("step %d: got %v, want %v", i, got, tc.want)
35+ }
36+ }
37+ // The held value sums the frames since the last step.
38+ *s = Server{}
39+ wheel(60, 0)
40+ if s.wheelHeld[0] != 7.5 {
41+ t.Fatal("held", s.wheelHeld[0])
42+ }
43+ if wheel(60, 0) != [2]int32{1, 0} || s.wheelHeld[0] != 0 || values[0] != 15 {
44+ t.Fatal("held after step", s.wheelHeld[0])
45+ }
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{} {
52+ t.Fatal("finger scroll made discrete steps")
53+ }
54+}
@@ -0,0 +1,54 @@
1+package wayland
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/nefertty/internal/ports"
7+)
8+
9+// High-resolution wheels send fractions of a detent: pre-v8 clients get a
10+// discrete step once 120 builds up; a direction change drops the rest.
11+func TestWheelSteps(t *testing.T) {
12+ s := &Server{}
13+ var values [2]float64
14+ wheel := func(v, h int32) (steps [2]int32) {
15+ steps, values = s.wheelSteps(ports.PointerAxis{
16+ Vertical: ports.ScrollAxis{Set: v != 0, V120: v, Value: float64(v) / 8},
17+ Horizontal: ports.ScrollAxis{Set: h != 0, V120: h},
18+ })
19+ return steps
20+ }
21+ for i, tc := range []struct {
22+ v, h int32
23+ want [2]int32
24+ }{
25+ {60, 0, [2]int32{}},
26+ {60, 0, [2]int32{1, 0}},
27+ {180, 0, [2]int32{1, 0}},
28+ {60, 0, [2]int32{1, 0}},
29+ {-60, 240, [2]int32{0, 2}},
30+ {-60, 0, [2]int32{-1, 0}},
31+ {30, -30, [2]int32{}},
32+ } {
33+ if got := wheel(tc.v, tc.h); got != tc.want {
34+ t.Fatalf("step %d: got %v, want %v", i, got, tc.want)
35+ }
36+ }
37+ // The held value sums the frames since the last step.
38+ *s = Server{}
39+ wheel(60, 0)
40+ if s.wheelHeld[0] != 7.5 {
41+ t.Fatal("held", s.wheelHeld[0])
42+ }
43+ if wheel(60, 0) != [2]int32{1, 0} || s.wheelHeld[0] != 0 || values[0] != 15 {
44+ t.Fatal("held after step", s.wheelHeld[0])
45+ }
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{} {
52+ t.Fatal("finger scroll made discrete steps")
53+ }
54+}
M internal/adapters/wayland/globals.go +1 -1
@@ -38,7 +38,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
3838 })
3939 },
4040 func() error {
41- return wayland.NewSeatGlobal(d, 7, func(c server.Client, v, id uint32) {
41+ return wayland.NewSeatGlobal(d, 8, func(c server.Client, v, id uint32) {
4242 r, e := wayland.NewSeat(c, int32(v), id, seat{s})
4343 if e == nil {
4444 capabilities := uint32(wayland.SeatCapabilityPointer)
@@ -38,7 +38,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
38 })38 })
39 },39 },
40 func() error {40 func() error {
41- return wayland.NewSeatGlobal(d, 7, func(c server.Client, v, id uint32) {41+ return wayland.NewSeatGlobal(d, 8, func(c server.Client, v, id uint32) {
42 r, e := wayland.NewSeat(c, int32(v), id, seat{s})42 r, e := wayland.NewSeat(c, int32(v), id, seat{s})
43 if e == nil {43 if e == nil {
44 capabilities := uint32(wayland.SeatCapabilityPointer)44 capabilities := uint32(wayland.SeatCapabilityPointer)
M internal/adapters/wayland/protocol_test.go +35 -10
@@ -3,6 +3,7 @@ package wayland
33 import (
44 "bytes"
55 "errors"
6+ "fmt"
67 "github.com/bnema/nefertty/internal/ports"
78 "os"
89 "path/filepath"
@@ -29,12 +30,17 @@ func protocolClient(t *testing.T, s *Server, dir string) *wlturbo.Display {
2930 }
3031
3132 func bindProtocol(t *testing.T, c *wlturbo.Display, iface string) uint32 {
33+ t.Helper()
34+ return bindVersion(t, c, iface, 1)
35+}
36+
37+func bindVersion(t *testing.T, c *wlturbo.Display, iface string, version uint32) uint32 {
3238 t.Helper()
3339 g, ok := c.Registry().FindGlobal(iface)
3440 if !ok {
3541 t.Fatalf("missing %s", iface)
3642 }
37- id, err := c.Registry().BindID(g.Name, g.Interface, 1)
43+ id, err := c.Registry().BindID(g.Name, g.Interface, version)
3844 if err != nil {
3945 t.Fatal(err)
4046 }
@@ -425,6 +431,7 @@ type pointerEvents struct {
425431 wlturbo.BaseProxy
426432 enters chan [2]float64
427433 buttons chan uint32
434+ axes chan string
428435 }
429436
430437 func (p *pointerEvents) Dispatch(e *wlturbo.Event) {
@@ -437,16 +444,26 @@ func (p *pointerEvents) Dispatch(e *wlturbo.Event) {
437444 _ = e.Uint32()
438445 _ = e.Uint32()
439446 p.buttons <- e.Uint32()
447+ case uint16(wayland.PointerEventAxisSource):
448+ p.axes <- fmt.Sprint("source ", e.Uint32())
449+ case uint16(wayland.PointerEventAxisValue120):
450+ p.axes <- fmt.Sprint("v120 ", e.Uint32(), " ", e.Int32())
451+ case uint16(wayland.PointerEventAxis):
452+ _ = e.Uint32()
453+ p.axes <- fmt.Sprint("axis ", e.Uint32(), " ", e.Fixed().Float64())
454+ case uint16(wayland.PointerEventAxisStop):
455+ _ = e.Uint32()
456+ p.axes <- fmt.Sprint("stop ", e.Uint32())
440457 }
441458 }
442459
443460 func TestPointerProtocol(t *testing.T) {
444461 s, events, commands, dir := lifecycleServer(t)
445462 c := protocolClient(t, s, dir)
446- seat := bindProtocol(t, c, "wl_seat")
463+ seat := bindVersion(t, c, "wl_seat", 8)
447464 registerProtocol(t, c, seat)
448465 pointer := c.AllocateID()
449- p := &pointerEvents{enters: make(chan [2]float64, 1), buttons: make(chan uint32, 1)}
466+ p := &pointerEvents{enters: make(chan [2]float64, 1), buttons: make(chan uint32, 8), axes: make(chan string, 16)}
450467 p.SetID(pointer)
451468 c.Context().Register(p)
452469 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
@@ -493,6 +510,12 @@ func TestPointerProtocol(t *testing.T) {
493510 w := mapped(t, events, 2*time.Second)
494511 commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 20}
495512 commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, TimeMsec: 1}
513+ // Middle, side and extra buttons pass through as evdev codes.
514+ for _, b := range []uint32{0x112, 0x113, 0x114} {
515+ commands <- ports.PointerButtonTo{ID: w.ID, Button: b, Pressed: true, TimeMsec: 1}
516+ }
517+ commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}, TimeMsec: 2}}
518+ commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, TimeMsec: 3}}
496519 deadline := time.After(2 * time.Second)
497520 dispatched := make(chan error, 1)
498521 go func() {
@@ -503,7 +526,7 @@ func TestPointerProtocol(t *testing.T) {
503526 }
504527 }
505528 }()
506- for len(p.enters) == 0 || len(p.buttons) == 0 {
529+ for len(p.enters) == 0 || len(p.buttons) < 4 || len(p.axes) < 5 {
507530 select {
508531 case err := <-dispatched:
509532 t.Fatal(err)
@@ -520,13 +543,15 @@ func TestPointerProtocol(t *testing.T) {
520543 default:
521544 t.Fatal("missing pointer enter")
522545 }
523- select {
524- case button := <-p.buttons:
525- if button != 0x110 {
526- t.Fatalf("button: %d", button)
546+ for _, want := range []uint32{0x110, 0x112, 0x113, 0x114} {
547+ if b := <-p.buttons; b != want {
548+ t.Fatalf("button: %#x, want %#x", b, want)
549+ }
550+ }
551+ for _, want := range []string{"source 0", "v120 0 120", "axis 0 15", "source 1", "stop 1"} {
552+ if got := <-p.axes; got != want {
553+ t.Fatalf("axis event %q, want %q", got, want)
527554 }
528- default:
529- t.Fatal("missing pointer button")
530555 }
531556 }
532557
@@ -3,6 +3,7 @@ package wayland
3 import (3 import (
4 "bytes"4 "bytes"
5 "errors"5 "errors"
6+ "fmt"
6 "github.com/bnema/nefertty/internal/ports"7 "github.com/bnema/nefertty/internal/ports"
7 "os"8 "os"
8 "path/filepath"9 "path/filepath"
@@ -29,12 +30,17 @@ func protocolClient(t *testing.T, s *Server, dir string) *wlturbo.Display {
29 }30 }
30 31
31 func bindProtocol(t *testing.T, c *wlturbo.Display, iface string) uint32 {32 func bindProtocol(t *testing.T, c *wlturbo.Display, iface string) uint32 {
33+ t.Helper()
34+ return bindVersion(t, c, iface, 1)
35+}
36+
37+func bindVersion(t *testing.T, c *wlturbo.Display, iface string, version uint32) uint32 {
32 t.Helper()38 t.Helper()
33 g, ok := c.Registry().FindGlobal(iface)39 g, ok := c.Registry().FindGlobal(iface)
34 if !ok {40 if !ok {
35 t.Fatalf("missing %s", iface)41 t.Fatalf("missing %s", iface)
36 }42 }
37- id, err := c.Registry().BindID(g.Name, g.Interface, 1)43+ id, err := c.Registry().BindID(g.Name, g.Interface, version)
38 if err != nil {44 if err != nil {
39 t.Fatal(err)45 t.Fatal(err)
40 }46 }
@@ -425,6 +431,7 @@ type pointerEvents struct {
425 wlturbo.BaseProxy431 wlturbo.BaseProxy
426 enters chan [2]float64432 enters chan [2]float64
427 buttons chan uint32433 buttons chan uint32
434+ axes chan string
428 }435 }
429 436
430 func (p *pointerEvents) Dispatch(e *wlturbo.Event) {437 func (p *pointerEvents) Dispatch(e *wlturbo.Event) {
@@ -437,16 +444,26 @@ func (p *pointerEvents) Dispatch(e *wlturbo.Event) {
437 _ = e.Uint32()444 _ = e.Uint32()
438 _ = e.Uint32()445 _ = e.Uint32()
439 p.buttons <- e.Uint32()446 p.buttons <- e.Uint32()
447+ case uint16(wayland.PointerEventAxisSource):
448+ p.axes <- fmt.Sprint("source ", e.Uint32())
449+ case uint16(wayland.PointerEventAxisValue120):
450+ p.axes <- fmt.Sprint("v120 ", e.Uint32(), " ", e.Int32())
451+ case uint16(wayland.PointerEventAxis):
452+ _ = e.Uint32()
453+ p.axes <- fmt.Sprint("axis ", e.Uint32(), " ", e.Fixed().Float64())
454+ case uint16(wayland.PointerEventAxisStop):
455+ _ = e.Uint32()
456+ p.axes <- fmt.Sprint("stop ", e.Uint32())
440 }457 }
441 }458 }
442 459
443 func TestPointerProtocol(t *testing.T) {460 func TestPointerProtocol(t *testing.T) {
444 s, events, commands, dir := lifecycleServer(t)461 s, events, commands, dir := lifecycleServer(t)
445 c := protocolClient(t, s, dir)462 c := protocolClient(t, s, dir)
446- seat := bindProtocol(t, c, "wl_seat")463+ seat := bindVersion(t, c, "wl_seat", 8)
447 registerProtocol(t, c, seat)464 registerProtocol(t, c, seat)
448 pointer := c.AllocateID()465 pointer := c.AllocateID()
449- p := &pointerEvents{enters: make(chan [2]float64, 1), buttons: make(chan uint32, 1)}466+ p := &pointerEvents{enters: make(chan [2]float64, 1), buttons: make(chan uint32, 8), axes: make(chan string, 16)}
450 p.SetID(pointer)467 p.SetID(pointer)
451 c.Context().Register(p)468 c.Context().Register(p)
452 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)469 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
@@ -493,6 +510,12 @@ func TestPointerProtocol(t *testing.T) {
493 w := mapped(t, events, 2*time.Second)510 w := mapped(t, events, 2*time.Second)
494 commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 20}511 commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 20}
495 commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, TimeMsec: 1}512 commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, TimeMsec: 1}
513+ // Middle, side and extra buttons pass through as evdev codes.
514+ for _, b := range []uint32{0x112, 0x113, 0x114} {
515+ commands <- ports.PointerButtonTo{ID: w.ID, Button: b, Pressed: true, TimeMsec: 1}
516+ }
517+ commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}, TimeMsec: 2}}
518+ commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, TimeMsec: 3}}
496 deadline := time.After(2 * time.Second)519 deadline := time.After(2 * time.Second)
497 dispatched := make(chan error, 1)520 dispatched := make(chan error, 1)
498 go func() {521 go func() {
@@ -503,7 +526,7 @@ func TestPointerProtocol(t *testing.T) {
503 }526 }
504 }527 }
505 }()528 }()
506- for len(p.enters) == 0 || len(p.buttons) == 0 {529+ for len(p.enters) == 0 || len(p.buttons) < 4 || len(p.axes) < 5 {
507 select {530 select {
508 case err := <-dispatched:531 case err := <-dispatched:
509 t.Fatal(err)532 t.Fatal(err)
@@ -520,13 +543,15 @@ func TestPointerProtocol(t *testing.T) {
520 default:543 default:
521 t.Fatal("missing pointer enter")544 t.Fatal("missing pointer enter")
522 }545 }
523- select {546+ for _, want := range []uint32{0x110, 0x112, 0x113, 0x114} {
524- case button := <-p.buttons:547+ if b := <-p.buttons; b != want {
525- if button != 0x110 {548+ t.Fatalf("button: %#x, want %#x", b, want)
526- t.Fatalf("button: %d", button)549+ }
550+ }
551+ for _, want := range []string{"source 0", "v120 0 120", "axis 0 15", "source 1", "stop 1"} {
552+ if got := <-p.axes; got != want {
553+ t.Fatalf("axis event %q, want %q", got, want)
527 }554 }
528- default:
529- t.Fatal("missing pointer button")
530 }555 }
531 }556 }
532 557
M internal/adapters/wayland/server.go +84 -0
@@ -82,6 +82,8 @@ type Server struct {
8282 nextPool uint64
8383 focused ports.WindowID
8484 pointerFocus ports.WindowID
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
8587 pointerX, pointerY float64
8688 pointers map[server.Client][]*wayland.Pointer
8789 modState ports.ModState
@@ -396,6 +398,15 @@ func (s *Server) apply(cmd ports.ClientCommand) {
396398 pointerFrame(p)
397399 }
398400 }
401+ case ports.PointerAxisTo:
402+ // Scroll applies to the entered surface only.
403+ if c.ID != s.pointerFocus {
404+ return
405+ }
406+ steps, values := s.wheelSteps(c.Axis)
407+ for _, p := range s.windowPointers(s.windows[c.ID]) {
408+ sendAxis(p, c.Axis, steps, values)
409+ }
399410 case ports.SetKeymap:
400411 s.setKeymap(c)
401412 case ports.FocusWindow:
@@ -606,6 +617,78 @@ func (s *Server) changeFocus(id ports.WindowID) {
606617 s.updateConstraint()
607618 }
608619
620+// wheelSteps adds wheel v120 to the rest per axis and returns the whole
621+// detents for axis_discrete (seat before v8). High-resolution wheels send
622+// fractions of 120; a direction change drops the rest, as wlroots does.
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) {
626+ if a.Source != ports.AxisWheel {
627+ return steps, values
628+ }
629+ for i, ax := range [2]ports.ScrollAxis{a.Vertical, a.Horizontal} {
630+ if !ax.Set || ax.V120 == 0 {
631+ continue
632+ }
633+ if (s.wheelRest[i] < 0) != (ax.V120 < 0) {
634+ s.wheelRest[i], s.wheelHeld[i] = 0, 0
635+ }
636+ s.wheelRest[i] += ax.V120
637+ s.wheelHeld[i] += ax.Value
638+ steps[i] = s.wheelRest[i] / 120
639+ s.wheelRest[i] -= steps[i] * 120
640+ if steps[i] != 0 {
641+ values[i], s.wheelHeld[i] = s.wheelHeld[i], 0
642+ }
643+ }
644+ return steps, values
645+}
646+
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 a
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) {
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+ }
662+ if v >= 5 {
663+ p.SendAxisSource(uint32(a.Source))
664+ }
665+ for axis, s := range axes {
666+ if !s.Set {
667+ continue
668+ }
669+ if s.Stop {
670+ if v >= 5 {
671+ p.SendAxisStop(a.TimeMsec, uint32(axis))
672+ }
673+ continue
674+ }
675+ value := s.Value
676+ if a.Source == ports.AxisWheel && s.V120 != 0 {
677+ switch {
678+ case v >= 8:
679+ p.SendAxisValue120(uint32(axis), s.V120)
680+ default:
681+ value = values[axis]
682+ if v >= 5 {
683+ p.SendAxisDiscrete(uint32(axis), steps[axis])
684+ }
685+ }
686+ }
687+ p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(value))
688+ }
689+ pointerFrame(p)
690+}
691+
609692 func pointerFrame(p *wayland.Pointer) {
610693 if p.Version() >= 5 {
611694 p.SendFrame()
@@ -627,6 +710,7 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
627710 if id == s.pointerFocus {
628711 return
629712 }
713+ s.wheelRest, s.wheelHeld = [2]int32{}, [2]float64{}
630714 // The new client sets its own cursor on enter; until then, the arrow.
631715 s.cursorSurface = nil
632716 s.setCursor(ports.CursorChange{})
@@ -82,6 +82,8 @@ type Server struct {
82 nextPool uint6482 nextPool uint64
83 focused ports.WindowID83 focused ports.WindowID
84 pointerFocus ports.WindowID84 pointerFocus ports.WindowID
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
85 pointerX, pointerY float6487 pointerX, pointerY float64
86 pointers map[server.Client][]*wayland.Pointer88 pointers map[server.Client][]*wayland.Pointer
87 modState ports.ModState89 modState ports.ModState
@@ -396,6 +398,15 @@ func (s *Server) apply(cmd ports.ClientCommand) {
396 pointerFrame(p)398 pointerFrame(p)
397 }399 }
398 }400 }
401+ case ports.PointerAxisTo:
402+ // Scroll applies to the entered surface only.
403+ if c.ID != s.pointerFocus {
404+ return
405+ }
406+ steps, values := s.wheelSteps(c.Axis)
407+ for _, p := range s.windowPointers(s.windows[c.ID]) {
408+ sendAxis(p, c.Axis, steps, values)
409+ }
399 case ports.SetKeymap:410 case ports.SetKeymap:
400 s.setKeymap(c)411 s.setKeymap(c)
401 case ports.FocusWindow:412 case ports.FocusWindow:
@@ -606,6 +617,78 @@ func (s *Server) changeFocus(id ports.WindowID) {
606 s.updateConstraint()617 s.updateConstraint()
607 }618 }
608 619
620+// wheelSteps adds wheel v120 to the rest per axis and returns the whole
621+// detents for axis_discrete (seat before v8). High-resolution wheels send
622+// fractions of 120; a direction change drops the rest, as wlroots does.
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) {
626+ if a.Source != ports.AxisWheel {
627+ return steps, values
628+ }
629+ for i, ax := range [2]ports.ScrollAxis{a.Vertical, a.Horizontal} {
630+ if !ax.Set || ax.V120 == 0 {
631+ continue
632+ }
633+ if (s.wheelRest[i] < 0) != (ax.V120 < 0) {
634+ s.wheelRest[i], s.wheelHeld[i] = 0, 0
635+ }
636+ s.wheelRest[i] += ax.V120
637+ s.wheelHeld[i] += ax.Value
638+ steps[i] = s.wheelRest[i] / 120
639+ s.wheelRest[i] -= steps[i] * 120
640+ if steps[i] != 0 {
641+ values[i], s.wheelHeld[i] = s.wheelHeld[i], 0
642+ }
643+ }
644+ return steps, values
645+}
646+
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 a
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) {
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+ }
662+ if v >= 5 {
663+ p.SendAxisSource(uint32(a.Source))
664+ }
665+ for axis, s := range axes {
666+ if !s.Set {
667+ continue
668+ }
669+ if s.Stop {
670+ if v >= 5 {
671+ p.SendAxisStop(a.TimeMsec, uint32(axis))
672+ }
673+ continue
674+ }
675+ value := s.Value
676+ if a.Source == ports.AxisWheel && s.V120 != 0 {
677+ switch {
678+ case v >= 8:
679+ p.SendAxisValue120(uint32(axis), s.V120)
680+ default:
681+ value = values[axis]
682+ if v >= 5 {
683+ p.SendAxisDiscrete(uint32(axis), steps[axis])
684+ }
685+ }
686+ }
687+ p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(value))
688+ }
689+ pointerFrame(p)
690+}
691+
609 func pointerFrame(p *wayland.Pointer) {692 func pointerFrame(p *wayland.Pointer) {
610 if p.Version() >= 5 {693 if p.Version() >= 5 {
611 p.SendFrame()694 p.SendFrame()
@@ -627,6 +710,7 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
627 if id == s.pointerFocus {710 if id == s.pointerFocus {
628 return711 return
629 }712 }
713+ s.wheelRest, s.wheelHeld = [2]int32{}, [2]float64{}
630 // The new client sets its own cursor on enter; until then, the arrow.714 // The new client sets its own cursor on enter; until then, the arrow.
631 s.cursorSurface = nil715 s.cursorSurface = nil
632 s.setCursor(ports.CursorChange{})716 s.setCursor(ports.CursorChange{})
M internal/core/core.go +9 -0
@@ -739,6 +739,15 @@ func (c *Core) Run(ctx context.Context) error {
739739 c.grab = 0
740740 }
741741 continue
742+ case ports.PointerAxis:
743+ // Scroll goes to the window under the pointer, which has the
744+ // pointer focus even mid-drag.
745+ if c.pointer != 0 {
746+ if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
747+ return nil
748+ }
749+ }
750+ continue
742751 }
743752 key, ok := ev.(ports.KeyEvent)
744753 if !ok {
@@ -739,6 +739,15 @@ func (c *Core) Run(ctx context.Context) error {
739 c.grab = 0739 c.grab = 0
740 }740 }
741 continue741 continue
742+ case ports.PointerAxis:
743+ // Scroll goes to the window under the pointer, which has the
744+ // pointer focus even mid-drag.
745+ if c.pointer != 0 {
746+ if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil {
747+ return nil
748+ }
749+ }
750+ continue
742 }751 }
743 key, ok := ev.(ports.KeyEvent)752 key, ok := ev.(ports.KeyEvent)
744 if !ok {753 if !ok {
M internal/core/core_test.go +7 -0
@@ -247,6 +247,10 @@ func TestPointerFocusAndGrab(t *testing.T) {
247247 if v := command(t, commands); v != (ports.PointerMotionTo{ID: 1, X: 2, Y: 3, TimeMsec: 1}) {
248248 t.Fatal(v)
249249 }
250+ input <- ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}}
251+ if v := command(t, commands); v != (ports.PointerAxisTo{ID: 1, Axis: ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}}}) {
252+ t.Fatal(v)
253+ }
250254 input <- ports.PointerButton{Button: 0x110, Pressed: true}
251255 if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true}) {
252256 t.Fatal(v)
@@ -265,6 +269,9 @@ func TestPointerFocusAndGrab(t *testing.T) {
265269 if v := command(t, commands); v != (ports.PointerFocus{}) {
266270 t.Fatal(v)
267271 }
272+ // Mid-drag off the window, scroll goes to what is under the pointer:
273+ // nothing here, not the grab window that got a leave.
274+ input <- ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 1}}
268275 input <- ports.PointerButton{Button: 0x110}
269276 if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110}) {
270277 t.Fatal(v)
@@ -247,6 +247,10 @@ func TestPointerFocusAndGrab(t *testing.T) {
247 if v := command(t, commands); v != (ports.PointerMotionTo{ID: 1, X: 2, Y: 3, TimeMsec: 1}) {247 if v := command(t, commands); v != (ports.PointerMotionTo{ID: 1, X: 2, Y: 3, TimeMsec: 1}) {
248 t.Fatal(v)248 t.Fatal(v)
249 }249 }
250+ input <- ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}}
251+ if v := command(t, commands); v != (ports.PointerAxisTo{ID: 1, Axis: ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}}}) {
252+ t.Fatal(v)
253+ }
250 input <- ports.PointerButton{Button: 0x110, Pressed: true}254 input <- ports.PointerButton{Button: 0x110, Pressed: true}
251 if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true}) {255 if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true}) {
252 t.Fatal(v)256 t.Fatal(v)
@@ -265,6 +269,9 @@ func TestPointerFocusAndGrab(t *testing.T) {
265 if v := command(t, commands); v != (ports.PointerFocus{}) {269 if v := command(t, commands); v != (ports.PointerFocus{}) {
266 t.Fatal(v)270 t.Fatal(v)
267 }271 }
272+ // Mid-drag off the window, scroll goes to what is under the pointer:
273+ // nothing here, not the grab window that got a leave.
274+ input <- ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 1}}
268 input <- ports.PointerButton{Button: 0x110}275 input <- ports.PointerButton{Button: 0x110}
269 if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110}) {276 if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110}) {
270 t.Fatal(v)277 t.Fatal(v)
M internal/ports/messages.go +36 -0
@@ -255,6 +255,34 @@ type PointerButton struct {
255255
256256 func (PointerButton) inputEvent() {}
257257
258+// AxisSource tells how a scroll was made; values match wl_pointer.axis_source.
259+type AxisSource uint8
260+
261+const (
262+ AxisWheel AxisSource = iota
263+ AxisFinger
264+ AxisContinuous
265+)
266+
267+// ScrollAxis is one scroll direction in a pointer frame. Value is in
268+// surface pixels; V120 counts wheel detents in 1/120 steps (wheel only).
269+// Stop ends a finger or continuous scroll on this axis.
270+type ScrollAxis struct {
271+ Set bool
272+ Value float64
273+ V120 int32
274+ Stop bool
275+}
276+
277+// PointerAxis is one scroll frame: Vertical, then Horizontal.
278+type PointerAxis struct {
279+ Source AxisSource
280+ Vertical, Horizontal ScrollAxis
281+ TimeMsec uint32
282+}
283+
284+func (PointerAxis) inputEvent() {}
285+
258286 // OutputEvent carries output → core notifications.
259287 type OutputEvent interface{ outputEvent() }
260288
@@ -440,6 +468,14 @@ type PointerButtonTo struct {
440468
441469 func (PointerButtonTo) clientCommand() {}
442470
471+// PointerAxisTo sends a scroll frame to a window.
472+type PointerAxisTo struct {
473+ ID WindowID
474+ Axis PointerAxis
475+}
476+
477+func (PointerAxisTo) clientCommand() {}
478+
443479 // ForwardKey carries core → wayland unbound keys.
444480 type ForwardKey struct {
445481 ID WindowID
@@ -255,6 +255,34 @@ type PointerButton struct {
255 255
256 func (PointerButton) inputEvent() {}256 func (PointerButton) inputEvent() {}
257 257
258+// AxisSource tells how a scroll was made; values match wl_pointer.axis_source.
259+type AxisSource uint8
260+
261+const (
262+ AxisWheel AxisSource = iota
263+ AxisFinger
264+ AxisContinuous
265+)
266+
267+// ScrollAxis is one scroll direction in a pointer frame. Value is in
268+// surface pixels; V120 counts wheel detents in 1/120 steps (wheel only).
269+// Stop ends a finger or continuous scroll on this axis.
270+type ScrollAxis struct {
271+ Set bool
272+ Value float64
273+ V120 int32
274+ Stop bool
275+}
276+
277+// PointerAxis is one scroll frame: Vertical, then Horizontal.
278+type PointerAxis struct {
279+ Source AxisSource
280+ Vertical, Horizontal ScrollAxis
281+ TimeMsec uint32
282+}
283+
284+func (PointerAxis) inputEvent() {}
285+
258 // OutputEvent carries output → core notifications.286 // OutputEvent carries output → core notifications.
259 type OutputEvent interface{ outputEvent() }287 type OutputEvent interface{ outputEvent() }
260 288
@@ -440,6 +468,14 @@ type PointerButtonTo struct {
440 468
441 func (PointerButtonTo) clientCommand() {}469 func (PointerButtonTo) clientCommand() {}
442 470
471+// PointerAxisTo sends a scroll frame to a window.
472+type PointerAxisTo struct {
473+ ID WindowID
474+ Axis PointerAxis
475+}
476+
477+func (PointerAxisTo) clientCommand() {}
478+
443 // ForwardKey carries core → wayland unbound keys.479 // ForwardKey carries core → wayland unbound keys.
444 type ForwardKey struct {480 type ForwardKey struct {
445 ID WindowID481 ID WindowID

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