feat(input): forward scroll wheel and finger scroll to clients
- commit
- 37d8478507b5936d3985616cbf795c07e69c1790
- author
- brice · · 2026-09-25 19:50 UTC
- parent
- 9bc7abf
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +148 -11
M internal/adapters/libinput/libinput.go +28 -0
| @@ -27,6 +27,9 @@ const ( | |||
| 27 | 27 | evPointerMotion = 400 | |
| 28 | 28 | evPointerAbs = 401 | |
| 29 | 29 | evPointerButton = 402 | |
| 30 | + | evScrollWheel = 404 | |
| 31 | + | evScrollFinger = 405 | |
| 32 | + | evScrollCont = 406 | |
| 30 | 33 | ) | |
| 31 | 34 | ||
| 32 | 35 | var ( | |
| @@ -60,6 +63,9 @@ var ( | |||
| 60 | 63 | pointerAbsY func(pev uintptr, height uint32) float64 | |
| 61 | 64 | pointerButton func(pev uintptr) uint32 | |
| 62 | 65 | 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 | 69 | iface [2]uintptr | |
| 64 | 70 | active ports.Seat | |
| 65 | 71 | ) | |
| @@ -109,6 +115,9 @@ func load() error { | |||
| 109 | 115 | reg(&pointerAbsY, "event_pointer_get_absolute_y_transformed") | |
| 110 | 116 | reg(&pointerButton, "event_pointer_get_button") | |
| 111 | 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 | 121 | iface[0] = purego.NewCallback(func(path *byte, _, _ uintptr) uintptr { | |
| 113 | 122 | fd, err := active.OpenDevice(goString(path)) | |
| 114 | 123 | if err != nil { | |
| @@ -286,6 +295,25 @@ func translate(ev uintptr, opts Options, p *pointer) (ports.InputEvent, error) { | |||
| 286 | 295 | b, pressed := pointerButton(pe), pointerBtnState(pe) == 1 | |
| 287 | 296 | log.Debug().Uint32("button", b).Bool("pressed", pressed).Msg("button") | |
| 288 | 297 | 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).Msg("scroll") | |
| 316 | + | return a, nil | |
| 289 | 317 | } | |
| 290 | 318 | return nil, nil | |
| 291 | 319 | } | |
| @@ -27,6 +27,9 @@ const ( | |||||
| 27 | evPointerMotion = 400 | 27 | evPointerMotion = 400 | ||
| 28 | evPointerAbs = 401 | 28 | evPointerAbs = 401 | ||
| 29 | evPointerButton = 402 | 29 | 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) float64 | 63 | pointerAbsY func(pev uintptr, height uint32) float64 | ||
| 61 | pointerButton func(pev uintptr) uint32 | 64 | pointerButton func(pev uintptr) uint32 | ||
| 62 | pointerBtnState func(pev uintptr) int32 | 65 | 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]uintptr | 69 | iface [2]uintptr | ||
| 64 | active ports.Seat | 70 | 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) == 1 | 295 | 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}, nil | 297 | 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).Msg("scroll") | |||
| 316 | + | return a, nil | |||
| 289 | } | 317 | } | ||
| 290 | return nil, nil | 318 | return nil, nil | ||
| 291 | } | 319 | } | ||
M internal/adapters/wayland/globals.go +1 -1
| @@ -38,7 +38,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||
| 38 | 38 | }) | |
| 39 | 39 | }, | |
| 40 | 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 | 42 | r, e := wayland.NewSeat(c, int32(v), id, seat{s}) | |
| 43 | 43 | if e == nil { | |
| 44 | 44 | 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 | |||
| 3 | 3 | import ( | |
| 4 | 4 | "bytes" | |
| 5 | 5 | "errors" | |
| 6 | + | "fmt" | |
| 6 | 7 | "github.com/bnema/nefertty/internal/ports" | |
| 7 | 8 | "os" | |
| 8 | 9 | "path/filepath" | |
| @@ -29,12 +30,17 @@ func protocolClient(t *testing.T, s *Server, dir string) *wlturbo.Display { | |||
| 29 | 30 | } | |
| 30 | 31 | ||
| 31 | 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 | 38 | t.Helper() | |
| 33 | 39 | g, ok := c.Registry().FindGlobal(iface) | |
| 34 | 40 | if !ok { | |
| 35 | 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 | 44 | if err != nil { | |
| 39 | 45 | t.Fatal(err) | |
| 40 | 46 | } | |
| @@ -425,6 +431,7 @@ type pointerEvents struct { | |||
| 425 | 431 | wlturbo.BaseProxy | |
| 426 | 432 | enters chan [2]float64 | |
| 427 | 433 | buttons chan uint32 | |
| 434 | + | axes chan string | |
| 428 | 435 | } | |
| 429 | 436 | ||
| 430 | 437 | func (p *pointerEvents) Dispatch(e *wlturbo.Event) { | |
| @@ -437,16 +444,26 @@ func (p *pointerEvents) Dispatch(e *wlturbo.Event) { | |||
| 437 | 444 | _ = e.Uint32() | |
| 438 | 445 | _ = e.Uint32() | |
| 439 | 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 | 460 | func TestPointerProtocol(t *testing.T) { | |
| 444 | 461 | s, events, commands, dir := lifecycleServer(t) | |
| 445 | 462 | c := protocolClient(t, s, dir) | |
| 446 | - | seat := bindProtocol(t, c, "wl_seat") | |
| 463 | + | seat := bindVersion(t, c, "wl_seat", 8) | |
| 447 | 464 | registerProtocol(t, c, seat) | |
| 448 | 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 | 467 | p.SetID(pointer) | |
| 451 | 468 | c.Context().Register(p) | |
| 452 | 469 | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |
| @@ -493,6 +510,12 @@ func TestPointerProtocol(t *testing.T) { | |||
| 493 | 510 | w := mapped(t, events, 2*time.Second) | |
| 494 | 511 | commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 20} | |
| 495 | 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 | 519 | deadline := time.After(2 * time.Second) | |
| 497 | 520 | dispatched := make(chan error, 1) | |
| 498 | 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 | 530 | select { | |
| 508 | 531 | case err := <-dispatched: | |
| 509 | 532 | t.Fatal(err) | |
| @@ -520,13 +543,15 @@ func TestPointerProtocol(t *testing.T) { | |||
| 520 | 543 | default: | |
| 521 | 544 | t.Fatal("missing pointer enter") | |
| 522 | 545 | } | |
| 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) | |
| 527 | 554 | } | |
| 528 | - | default: | |
| 529 | - | t.Fatal("missing pointer button") | |
| 530 | 555 | } | |
| 531 | 556 | } | |
| 532 | 557 | ||
| @@ -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.BaseProxy | 431 | wlturbo.BaseProxy | ||
| 426 | enters chan [2]float64 | 432 | enters chan [2]float64 | ||
| 427 | buttons chan uint32 | 433 | 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 +32 -0
| @@ -396,6 +396,10 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 396 | 396 | pointerFrame(p) | |
| 397 | 397 | } | |
| 398 | 398 | } | |
| 399 | + | case ports.PointerAxisTo: | |
| 400 | + | for _, p := range s.windowPointers(s.windows[c.ID]) { | |
| 401 | + | sendAxis(p, c.Axis) | |
| 402 | + | } | |
| 399 | 403 | case ports.SetKeymap: | |
| 400 | 404 | s.setKeymap(c) | |
| 401 | 405 | case ports.FocusWindow: | |
| @@ -606,6 +610,34 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||
| 606 | 610 | s.updateConstraint() | |
| 607 | 611 | } | |
| 608 | 612 | ||
| 613 | + | // sendAxis sends one scroll frame, each event gated by the pointer version. | |
| 614 | + | func sendAxis(p *wayland.Pointer, a ports.PointerAxis) { | |
| 615 | + | v := p.Version() | |
| 616 | + | if v >= 5 { | |
| 617 | + | p.SendAxisSource(uint32(a.Source)) | |
| 618 | + | } | |
| 619 | + | for axis, s := range [2]ports.ScrollAxis{a.Vertical, a.Horizontal} { | |
| 620 | + | if !s.Set { | |
| 621 | + | continue | |
| 622 | + | } | |
| 623 | + | if s.Stop { | |
| 624 | + | if v >= 5 { | |
| 625 | + | p.SendAxisStop(a.TimeMsec, uint32(axis)) | |
| 626 | + | } | |
| 627 | + | continue | |
| 628 | + | } | |
| 629 | + | if a.Source == ports.AxisWheel && s.V120 != 0 { | |
| 630 | + | if v >= 8 { | |
| 631 | + | p.SendAxisValue120(uint32(axis), s.V120) | |
| 632 | + | } else if v >= 5 && s.V120/120 != 0 { | |
| 633 | + | p.SendAxisDiscrete(uint32(axis), s.V120/120) | |
| 634 | + | } | |
| 635 | + | } | |
| 636 | + | p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(s.Value)) | |
| 637 | + | } | |
| 638 | + | pointerFrame(p) | |
| 639 | + | } | |
| 640 | + | ||
| 609 | 641 | func pointerFrame(p *wayland.Pointer) { | |
| 610 | 642 | if p.Version() >= 5 { | |
| 611 | 643 | p.SendFrame() | |
| @@ -396,6 +396,10 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 396 | pointerFrame(p) | 396 | pointerFrame(p) | ||
| 397 | } | 397 | } | ||
| 398 | } | 398 | } | ||
| 399 | + | case ports.PointerAxisTo: | |||
| 400 | + | for _, p := range s.windowPointers(s.windows[c.ID]) { | |||
| 401 | + | sendAxis(p, c.Axis) | |||
| 402 | + | } | |||
| 399 | case ports.SetKeymap: | 403 | case ports.SetKeymap: | ||
| 400 | s.setKeymap(c) | 404 | s.setKeymap(c) | ||
| 401 | case ports.FocusWindow: | 405 | case ports.FocusWindow: | ||
| @@ -606,6 +610,34 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||||
| 606 | s.updateConstraint() | 610 | s.updateConstraint() | ||
| 607 | } | 611 | } | ||
| 608 | 612 | ||||
| 613 | + | // sendAxis sends one scroll frame, each event gated by the pointer version. | |||
| 614 | + | func sendAxis(p *wayland.Pointer, a ports.PointerAxis) { | |||
| 615 | + | v := p.Version() | |||
| 616 | + | if v >= 5 { | |||
| 617 | + | p.SendAxisSource(uint32(a.Source)) | |||
| 618 | + | } | |||
| 619 | + | for axis, s := range [2]ports.ScrollAxis{a.Vertical, a.Horizontal} { | |||
| 620 | + | if !s.Set { | |||
| 621 | + | continue | |||
| 622 | + | } | |||
| 623 | + | if s.Stop { | |||
| 624 | + | if v >= 5 { | |||
| 625 | + | p.SendAxisStop(a.TimeMsec, uint32(axis)) | |||
| 626 | + | } | |||
| 627 | + | continue | |||
| 628 | + | } | |||
| 629 | + | if a.Source == ports.AxisWheel && s.V120 != 0 { | |||
| 630 | + | if v >= 8 { | |||
| 631 | + | p.SendAxisValue120(uint32(axis), s.V120) | |||
| 632 | + | } else if v >= 5 && s.V120/120 != 0 { | |||
| 633 | + | p.SendAxisDiscrete(uint32(axis), s.V120/120) | |||
| 634 | + | } | |||
| 635 | + | } | |||
| 636 | + | p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(s.Value)) | |||
| 637 | + | } | |||
| 638 | + | pointerFrame(p) | |||
| 639 | + | } | |||
| 640 | + | ||||
| 609 | func pointerFrame(p *wayland.Pointer) { | 641 | func pointerFrame(p *wayland.Pointer) { | ||
| 610 | if p.Version() >= 5 { | 642 | if p.Version() >= 5 { | ||
| 611 | p.SendFrame() | 643 | p.SendFrame() | ||
M internal/core/core.go +12 -0
| @@ -739,6 +739,18 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 739 | 739 | c.grab = 0 | |
| 740 | 740 | } | |
| 741 | 741 | continue | |
| 742 | + | case ports.PointerAxis: | |
| 743 | + | // Scroll goes where a button would: the grab, else under the pointer. | |
| 744 | + | id := c.pointer | |
| 745 | + | if c.grab != 0 { | |
| 746 | + | id = c.grab | |
| 747 | + | } | |
| 748 | + | if id != 0 { | |
| 749 | + | if err := c.command(ctx, ports.PointerAxisTo{ID: id, Axis: v}); err != nil { | |
| 750 | + | return nil | |
| 751 | + | } | |
| 752 | + | } | |
| 753 | + | continue | |
| 742 | 754 | } | |
| 743 | 755 | key, ok := ev.(ports.KeyEvent) | |
| 744 | 756 | if !ok { | |
| @@ -739,6 +739,18 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 739 | c.grab = 0 | 739 | c.grab = 0 | ||
| 740 | } | 740 | } | ||
| 741 | continue | 741 | continue | ||
| 742 | + | case ports.PointerAxis: | |||
| 743 | + | // Scroll goes where a button would: the grab, else under the pointer. | |||
| 744 | + | id := c.pointer | |||
| 745 | + | if c.grab != 0 { | |||
| 746 | + | id = c.grab | |||
| 747 | + | } | |||
| 748 | + | if id != 0 { | |||
| 749 | + | if err := c.command(ctx, ports.PointerAxisTo{ID: id, Axis: v}); err != nil { | |||
| 750 | + | return nil | |||
| 751 | + | } | |||
| 752 | + | } | |||
| 753 | + | continue | |||
| 742 | } | 754 | } | ||
| 743 | key, ok := ev.(ports.KeyEvent) | 755 | key, ok := ev.(ports.KeyEvent) | ||
| 744 | if !ok { | 756 | if !ok { | ||
M internal/core/core_test.go +4 -0
| @@ -247,6 +247,10 @@ func TestPointerFocusAndGrab(t *testing.T) { | |||
| 247 | 247 | if v := command(t, commands); v != (ports.PointerMotionTo{ID: 1, X: 2, Y: 3, TimeMsec: 1}) { | |
| 248 | 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 | 254 | input <- ports.PointerButton{Button: 0x110, Pressed: true} | |
| 251 | 255 | if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true}) { | |
| 252 | 256 | 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) | ||
M internal/ports/messages.go +36 -0
| @@ -255,6 +255,34 @@ type PointerButton struct { | |||
| 255 | 255 | ||
| 256 | 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 | 286 | // OutputEvent carries output → core notifications. | |
| 259 | 287 | type OutputEvent interface{ outputEvent() } | |
| 260 | 288 | ||
| @@ -440,6 +468,14 @@ type PointerButtonTo struct { | |||
| 440 | 468 | ||
| 441 | 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 | 479 | // ForwardKey carries core → wayland unbound keys. | |
| 444 | 480 | type ForwardKey struct { | |
| 445 | 481 | 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 WindowID | 481 | ID WindowID | ||