merge: mouse (scroll wheel and all pointer buttons)
8 files · +254 -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).Int32("hv120", a.Horizontal.V120).Uint8("source", uint8(a.Source)).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).Int32("hv120", a.Horizontal.V120).Uint8("source", uint8(a.Source)).Msg("scroll") | |||
| 316 | + | return a, nil | |||
| 289 | } | 317 | } | ||
| 290 | return nil, nil | 318 | 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 { | |||
| 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 +84 -0
| @@ -82,6 +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 | |
| 86 | + | wheelHeld [2]float64 // axis value held back with it for pre-v8 clients | |
| 85 | 87 | pointerX, pointerY float64 | |
| 86 | 88 | pointers map[server.Client][]*wayland.Pointer | |
| 87 | 89 | modState ports.ModState | |
| @@ -396,6 +398,15 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 396 | 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 | 410 | case ports.SetKeymap: | |
| 400 | 411 | s.setKeymap(c) | |
| 401 | 412 | case ports.FocusWindow: | |
| @@ -606,6 +617,78 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||
| 606 | 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 | 692 | func pointerFrame(p *wayland.Pointer) { | |
| 610 | 693 | if p.Version() >= 5 { | |
| 611 | 694 | p.SendFrame() | |
| @@ -627,6 +710,7 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) { | |||
| 627 | 710 | if id == s.pointerFocus { | |
| 628 | 711 | return | |
| 629 | 712 | } | |
| 713 | + | s.wheelRest, s.wheelHeld = [2]int32{}, [2]float64{} | |
| 630 | 714 | // The new client sets its own cursor on enter; until then, the arrow. | |
| 631 | 715 | s.cursorSurface = nil | |
| 632 | 716 | s.setCursor(ports.CursorChange{}) | |
| @@ -82,6 +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 | |||
| 86 | + | wheelHeld [2]float64 // axis value held back with it for pre-v8 clients | |||
| 85 | pointerX, pointerY float64 | 87 | pointerX, pointerY float64 | ||
| 86 | pointers map[server.Client][]*wayland.Pointer | 88 | pointers map[server.Client][]*wayland.Pointer | ||
| 87 | modState ports.ModState | 89 | 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 | return | 711 | 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 = nil | 715 | 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 { | |||
| 739 | 739 | c.grab = 0 | |
| 740 | 740 | } | |
| 741 | 741 | 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 | 752 | key, ok := ev.(ports.KeyEvent) | |
| 744 | 753 | if !ok { | |
| @@ -739,6 +739,15 @@ 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 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) { | |||
| 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) | |
| @@ -265,6 +269,9 @@ func TestPointerFocusAndGrab(t *testing.T) { | |||
| 265 | 269 | if v := command(t, commands); v != (ports.PointerFocus{}) { | |
| 266 | 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 | 275 | input <- ports.PointerButton{Button: 0x110} | |
| 269 | 276 | if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110}) { | |
| 270 | 277 | 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 { | |||
| 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 | ||