detached 9c0be44

feat(wayland): serve zwp_pointer_gestures_v1 to clients

commit
9c0be447afc151511a6ef0d769dc131b0446f48a
author
brice · · 2026-10-08 09:32 UTC
committer
brice · · 2026-10-08 09:59 UTC
parent
9447c77
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +432 -5
M docs/config.md +2 -0
@@ -152,6 +152,8 @@ Over the shown stash a sideways swipe slides it with the fingers, with a stop on
152152
153153 Two-finger scroll goes to the window under the pointer with the touchpad's timestamps, so apps with kinetic scrolling keep their inertia.
154154
155+Three- and four-finger swipes belong to the compositor. Every other touchpad gesture goes to the window under the pointer through `zwp_pointer_gestures_v1` (version 3): swipes of other finger counts, pinch (zoom and rotation) and hold. A gesture stays with the window that had the pointer when it began; it ends as cancelled if the pointer moves to another window or the window closes. Nothing is sent to windows while the session is locked.
156+
155157 ## HDR
156158
157159 `output.<name>.hdr = on` sends HDR10 (BT.2020, PQ) to displays whose EDID and connector support it. The desktop stays SDR content, shown at `sdr-brightness` nits.
@@ -152,6 +152,8 @@ Over the shown stash a sideways swipe slides it with the fingers, with a stop on
152 152
153 Two-finger scroll goes to the window under the pointer with the touchpad's timestamps, so apps with kinetic scrolling keep their inertia.153 Two-finger scroll goes to the window under the pointer with the touchpad's timestamps, so apps with kinetic scrolling keep their inertia.
154 154
155+Three- and four-finger swipes belong to the compositor. Every other touchpad gesture goes to the window under the pointer through `zwp_pointer_gestures_v1` (version 3): swipes of other finger counts, pinch (zoom and rotation) and hold. A gesture stays with the window that had the pointer when it began; it ends as cancelled if the pointer moves to another window or the window closes. Nothing is sent to windows while the session is locked.
156+
155 ## HDR157 ## HDR
156 158
157 `output.<name>.hdr = on` sends HDR10 (BT.2020, PQ) to displays whose EDID and connector support it. The desktop stays SDR content, shown at `sdr-brightness` nits.159 `output.<name>.hdr = on` sends HDR10 (BT.2020, PQ) to displays whose EDID and connector support it. The desktop stays SDR content, shown at `sdr-brightness` nits.
A internal/adapters/wayland/gestures.go +220 -0
@@ -0,0 +1,220 @@
1+package wayland
2+
3+import (
4+ "time"
5+
6+ "github.com/bnema/go-wayland-bindings/server/pointergestures"
7+ "github.com/bnema/go-wayland-bindings/server/wayland"
8+ "github.com/bnema/neferwl/internal/ports"
9+ "github.com/bnema/purego-libwayland/server"
10+)
11+
12+// zwp_pointer_gestures_v1: touchpad swipes (other than the compositor's own
13+// three and four fingers), pinches and holds go to the client under the
14+// pointer. A gesture belongs to the window that had the pointer when it
15+// began; it ends cancelled when the pointer leaves that window.
16+
17+// gestureObject is one swipe, pinch or hold object of a client. Exactly one
18+// of the three is set.
19+type gestureObject struct {
20+ swipe *pointergestures.ZwpPointerGestureSwipeV1
21+ pinch *pointergestures.ZwpPointerGesturePinchV1
22+ hold *pointergestures.ZwpPointerGestureHoldV1
23+}
24+
25+func (g *gestureObject) alive() bool {
26+ switch {
27+ case g.swipe != nil:
28+ return g.swipe.Resource.Alive()
29+ case g.pinch != nil:
30+ return g.pinch.Resource.Alive()
31+ case g.hold != nil:
32+ return g.hold.Resource.Alive()
33+ }
34+ return false
35+}
36+
37+func (g *gestureObject) kind() ports.GestureKind {
38+ switch {
39+ case g.pinch != nil:
40+ return ports.GesturePinch
41+ case g.hold != nil:
42+ return ports.GestureHold
43+ }
44+ return ports.GestureSwipe
45+}
46+
47+func (g *gestureObject) begin(serial uint32, at uint32, surf *wayland.Surface, fingers uint32) {
48+ switch {
49+ case g.swipe != nil:
50+ g.swipe.SendBegin(serial, at, surf, fingers)
51+ case g.pinch != nil:
52+ g.pinch.SendBegin(serial, at, surf, fingers)
53+ case g.hold != nil:
54+ g.hold.SendBegin(serial, at, surf, fingers)
55+ }
56+}
57+
58+func (g *gestureObject) update(u ports.GestureUpdateTo) {
59+ at := wireMsec(u.Time)
60+ dx, dy := server.FixedFromFloat(u.DX), server.FixedFromFloat(u.DY)
61+ switch {
62+ case g.swipe != nil:
63+ g.swipe.SendUpdate(at, dx, dy)
64+ case g.pinch != nil:
65+ g.pinch.SendUpdate(at, dx, dy, server.FixedFromFloat(u.Scale), server.FixedFromFloat(u.Rotation))
66+ }
67+}
68+
69+func (g *gestureObject) end(serial uint32, at uint32, cancelled int32) {
70+ switch {
71+ case g.swipe != nil:
72+ g.swipe.SendEnd(serial, at, cancelled)
73+ case g.pinch != nil:
74+ g.pinch.SendEnd(serial, at, cancelled)
75+ case g.hold != nil:
76+ g.hold.SendEnd(serial, at, cancelled)
77+ }
78+}
79+
80+// activeGesture is the gesture sent to a window: the objects that got its
81+// begin are the ones that get the rest, even when a client makes more
82+// meanwhile.
83+type activeGesture struct {
84+ id ports.WindowID
85+ kind ports.GestureKind
86+ objs []*gestureObject
87+ // at is the latest device time sent, for a cancellation.
88+ at time.Duration
89+}
90+
91+func registerPointerGestures(d *server.Display, s *Server) error {
92+ return pointergestures.NewZwpPointerGesturesV1Global(d, 3, func(c server.Client, v, id uint32) {
93+ _, _ = pointergestures.NewZwpPointerGesturesV1(c, int32(v), id, gestureManager{s})
94+ })
95+}
96+
97+// gestureManager is zwp_pointer_gestures_v1. Releasing it leaves the
98+// gesture objects it made alive, as the protocol says.
99+type gestureManager struct{ server *Server }
100+
101+func (gestureManager) Release(*pointergestures.ZwpPointerGesturesV1) {}
102+
103+// gestureDestroy handles the destroy request of the three gesture objects;
104+// the resource's OnDestroy forgets the object.
105+type gestureDestroy struct{}
106+
107+func (gestureDestroy) Destroy(*pointergestures.ZwpPointerGestureSwipeV1) {}
108+
109+type pinchDestroy struct{}
110+
111+func (pinchDestroy) Destroy(*pointergestures.ZwpPointerGesturePinchV1) {}
112+
113+type holdDestroy struct{}
114+
115+func (holdDestroy) Destroy(*pointergestures.ZwpPointerGestureHoldV1) {}
116+
117+func (m gestureManager) GetSwipeGesture(r *pointergestures.ZwpPointerGesturesV1, id uint32, _ *wayland.Pointer) {
118+ if res, err := pointergestures.NewZwpPointerGestureSwipeV1(r.Client(), r.Version(), id, gestureDestroy{}); err == nil {
119+ m.server.addGesture(r.Client(), &gestureObject{swipe: res}, res.Resource)
120+ }
121+}
122+
123+func (m gestureManager) GetPinchGesture(r *pointergestures.ZwpPointerGesturesV1, id uint32, _ *wayland.Pointer) {
124+ if res, err := pointergestures.NewZwpPointerGesturePinchV1(r.Client(), r.Version(), id, pinchDestroy{}); err == nil {
125+ m.server.addGesture(r.Client(), &gestureObject{pinch: res}, res.Resource)
126+ }
127+}
128+
129+func (m gestureManager) GetHoldGesture(r *pointergestures.ZwpPointerGesturesV1, id uint32, _ *wayland.Pointer) {
130+ if res, err := pointergestures.NewZwpPointerGestureHoldV1(r.Client(), r.Version(), id, holdDestroy{}); err == nil {
131+ m.server.addGesture(r.Client(), &gestureObject{hold: res}, res.Resource)
132+ }
133+}
134+
135+// addGesture lists a client's gesture object until its resource dies.
136+func (s *Server) addGesture(c server.Client, o *gestureObject, res *server.Resource) {
137+ s.gestures[c] = append(s.gestures[c], o)
138+ res.OnDestroy = func() {
139+ if list := removeItem(s.gestures[c], o); len(list) > 0 {
140+ s.gestures[c] = list
141+ } else {
142+ delete(s.gestures, c)
143+ }
144+ }
145+}
146+
147+// beginGesture sends the begin of a gesture to the objects of the kind that
148+// the client of the pointer-focus window made. A gesture still running
149+// ends cancelled first. Nothing is recorded when the client has no object.
150+func (s *Server) beginGesture(c ports.GestureBeginTo) {
151+ s.cancelGesture()
152+ if c.ID != s.seat.pointerFocus || c.Fingers <= 0 {
153+ return
154+ }
155+ surf, _, _, _ := s.pointerSurface(c.ID, 0, 0)
156+ if surf == nil {
157+ return
158+ }
159+ var objs []*gestureObject
160+ for _, o := range s.gestures[surf.Client()] {
161+ if o.kind() == c.Kind && o.alive() {
162+ objs = append(objs, o)
163+ }
164+ }
165+ if len(objs) == 0 {
166+ return
167+ }
168+ s.seat.gesture = &activeGesture{id: c.ID, kind: c.Kind, objs: objs, at: c.Time}
169+ s.serial++
170+ for _, o := range objs {
171+ o.begin(s.serial, wireMsec(c.Time), surf, uint32(c.Fingers))
172+ }
173+}
174+
175+// updateGesture moves the running gesture of the window.
176+func (s *Server) updateGesture(c ports.GestureUpdateTo) {
177+ g := s.seat.gesture
178+ if g == nil || g.id != c.ID || g.kind != c.Kind {
179+ return
180+ }
181+ g.at = c.Time
182+ for _, o := range g.objs {
183+ if o.alive() {
184+ o.update(c)
185+ }
186+ }
187+}
188+
189+// endGesture ends the running gesture of the window.
190+func (s *Server) endGesture(c ports.GestureEndTo) {
191+ g := s.seat.gesture
192+ if g == nil || g.id != c.ID || g.kind != c.Kind {
193+ return
194+ }
195+ g.at = c.Time
196+ s.finishGesture(c.Cancelled)
197+}
198+
199+// cancelGesture ends the running gesture, if any, as cancelled: the
200+// pointer left its window or the window went.
201+func (s *Server) cancelGesture() {
202+ if s.seat.gesture != nil {
203+ s.finishGesture(true)
204+ }
205+}
206+
207+func (s *Server) finishGesture(cancelled bool) {
208+ g := s.seat.gesture
209+ s.seat.gesture = nil
210+ state := int32(0)
211+ if cancelled {
212+ state = 1
213+ }
214+ s.serial++
215+ for _, o := range g.objs {
216+ if o.alive() {
217+ o.end(s.serial, wireMsec(g.at), state)
218+ }
219+ }
220+}
@@ -0,0 +1,220 @@
1+package wayland
2+
3+import (
4+ "time"
5+
6+ "github.com/bnema/go-wayland-bindings/server/pointergestures"
7+ "github.com/bnema/go-wayland-bindings/server/wayland"
8+ "github.com/bnema/neferwl/internal/ports"
9+ "github.com/bnema/purego-libwayland/server"
10+)
11+
12+// zwp_pointer_gestures_v1: touchpad swipes (other than the compositor's own
13+// three and four fingers), pinches and holds go to the client under the
14+// pointer. A gesture belongs to the window that had the pointer when it
15+// began; it ends cancelled when the pointer leaves that window.
16+
17+// gestureObject is one swipe, pinch or hold object of a client. Exactly one
18+// of the three is set.
19+type gestureObject struct {
20+ swipe *pointergestures.ZwpPointerGestureSwipeV1
21+ pinch *pointergestures.ZwpPointerGesturePinchV1
22+ hold *pointergestures.ZwpPointerGestureHoldV1
23+}
24+
25+func (g *gestureObject) alive() bool {
26+ switch {
27+ case g.swipe != nil:
28+ return g.swipe.Resource.Alive()
29+ case g.pinch != nil:
30+ return g.pinch.Resource.Alive()
31+ case g.hold != nil:
32+ return g.hold.Resource.Alive()
33+ }
34+ return false
35+}
36+
37+func (g *gestureObject) kind() ports.GestureKind {
38+ switch {
39+ case g.pinch != nil:
40+ return ports.GesturePinch
41+ case g.hold != nil:
42+ return ports.GestureHold
43+ }
44+ return ports.GestureSwipe
45+}
46+
47+func (g *gestureObject) begin(serial uint32, at uint32, surf *wayland.Surface, fingers uint32) {
48+ switch {
49+ case g.swipe != nil:
50+ g.swipe.SendBegin(serial, at, surf, fingers)
51+ case g.pinch != nil:
52+ g.pinch.SendBegin(serial, at, surf, fingers)
53+ case g.hold != nil:
54+ g.hold.SendBegin(serial, at, surf, fingers)
55+ }
56+}
57+
58+func (g *gestureObject) update(u ports.GestureUpdateTo) {
59+ at := wireMsec(u.Time)
60+ dx, dy := server.FixedFromFloat(u.DX), server.FixedFromFloat(u.DY)
61+ switch {
62+ case g.swipe != nil:
63+ g.swipe.SendUpdate(at, dx, dy)
64+ case g.pinch != nil:
65+ g.pinch.SendUpdate(at, dx, dy, server.FixedFromFloat(u.Scale), server.FixedFromFloat(u.Rotation))
66+ }
67+}
68+
69+func (g *gestureObject) end(serial uint32, at uint32, cancelled int32) {
70+ switch {
71+ case g.swipe != nil:
72+ g.swipe.SendEnd(serial, at, cancelled)
73+ case g.pinch != nil:
74+ g.pinch.SendEnd(serial, at, cancelled)
75+ case g.hold != nil:
76+ g.hold.SendEnd(serial, at, cancelled)
77+ }
78+}
79+
80+// activeGesture is the gesture sent to a window: the objects that got its
81+// begin are the ones that get the rest, even when a client makes more
82+// meanwhile.
83+type activeGesture struct {
84+ id ports.WindowID
85+ kind ports.GestureKind
86+ objs []*gestureObject
87+ // at is the latest device time sent, for a cancellation.
88+ at time.Duration
89+}
90+
91+func registerPointerGestures(d *server.Display, s *Server) error {
92+ return pointergestures.NewZwpPointerGesturesV1Global(d, 3, func(c server.Client, v, id uint32) {
93+ _, _ = pointergestures.NewZwpPointerGesturesV1(c, int32(v), id, gestureManager{s})
94+ })
95+}
96+
97+// gestureManager is zwp_pointer_gestures_v1. Releasing it leaves the
98+// gesture objects it made alive, as the protocol says.
99+type gestureManager struct{ server *Server }
100+
101+func (gestureManager) Release(*pointergestures.ZwpPointerGesturesV1) {}
102+
103+// gestureDestroy handles the destroy request of the three gesture objects;
104+// the resource's OnDestroy forgets the object.
105+type gestureDestroy struct{}
106+
107+func (gestureDestroy) Destroy(*pointergestures.ZwpPointerGestureSwipeV1) {}
108+
109+type pinchDestroy struct{}
110+
111+func (pinchDestroy) Destroy(*pointergestures.ZwpPointerGesturePinchV1) {}
112+
113+type holdDestroy struct{}
114+
115+func (holdDestroy) Destroy(*pointergestures.ZwpPointerGestureHoldV1) {}
116+
117+func (m gestureManager) GetSwipeGesture(r *pointergestures.ZwpPointerGesturesV1, id uint32, _ *wayland.Pointer) {
118+ if res, err := pointergestures.NewZwpPointerGestureSwipeV1(r.Client(), r.Version(), id, gestureDestroy{}); err == nil {
119+ m.server.addGesture(r.Client(), &gestureObject{swipe: res}, res.Resource)
120+ }
121+}
122+
123+func (m gestureManager) GetPinchGesture(r *pointergestures.ZwpPointerGesturesV1, id uint32, _ *wayland.Pointer) {
124+ if res, err := pointergestures.NewZwpPointerGesturePinchV1(r.Client(), r.Version(), id, pinchDestroy{}); err == nil {
125+ m.server.addGesture(r.Client(), &gestureObject{pinch: res}, res.Resource)
126+ }
127+}
128+
129+func (m gestureManager) GetHoldGesture(r *pointergestures.ZwpPointerGesturesV1, id uint32, _ *wayland.Pointer) {
130+ if res, err := pointergestures.NewZwpPointerGestureHoldV1(r.Client(), r.Version(), id, holdDestroy{}); err == nil {
131+ m.server.addGesture(r.Client(), &gestureObject{hold: res}, res.Resource)
132+ }
133+}
134+
135+// addGesture lists a client's gesture object until its resource dies.
136+func (s *Server) addGesture(c server.Client, o *gestureObject, res *server.Resource) {
137+ s.gestures[c] = append(s.gestures[c], o)
138+ res.OnDestroy = func() {
139+ if list := removeItem(s.gestures[c], o); len(list) > 0 {
140+ s.gestures[c] = list
141+ } else {
142+ delete(s.gestures, c)
143+ }
144+ }
145+}
146+
147+// beginGesture sends the begin of a gesture to the objects of the kind that
148+// the client of the pointer-focus window made. A gesture still running
149+// ends cancelled first. Nothing is recorded when the client has no object.
150+func (s *Server) beginGesture(c ports.GestureBeginTo) {
151+ s.cancelGesture()
152+ if c.ID != s.seat.pointerFocus || c.Fingers <= 0 {
153+ return
154+ }
155+ surf, _, _, _ := s.pointerSurface(c.ID, 0, 0)
156+ if surf == nil {
157+ return
158+ }
159+ var objs []*gestureObject
160+ for _, o := range s.gestures[surf.Client()] {
161+ if o.kind() == c.Kind && o.alive() {
162+ objs = append(objs, o)
163+ }
164+ }
165+ if len(objs) == 0 {
166+ return
167+ }
168+ s.seat.gesture = &activeGesture{id: c.ID, kind: c.Kind, objs: objs, at: c.Time}
169+ s.serial++
170+ for _, o := range objs {
171+ o.begin(s.serial, wireMsec(c.Time), surf, uint32(c.Fingers))
172+ }
173+}
174+
175+// updateGesture moves the running gesture of the window.
176+func (s *Server) updateGesture(c ports.GestureUpdateTo) {
177+ g := s.seat.gesture
178+ if g == nil || g.id != c.ID || g.kind != c.Kind {
179+ return
180+ }
181+ g.at = c.Time
182+ for _, o := range g.objs {
183+ if o.alive() {
184+ o.update(c)
185+ }
186+ }
187+}
188+
189+// endGesture ends the running gesture of the window.
190+func (s *Server) endGesture(c ports.GestureEndTo) {
191+ g := s.seat.gesture
192+ if g == nil || g.id != c.ID || g.kind != c.Kind {
193+ return
194+ }
195+ g.at = c.Time
196+ s.finishGesture(c.Cancelled)
197+}
198+
199+// cancelGesture ends the running gesture, if any, as cancelled: the
200+// pointer left its window or the window went.
201+func (s *Server) cancelGesture() {
202+ if s.seat.gesture != nil {
203+ s.finishGesture(true)
204+ }
205+}
206+
207+func (s *Server) finishGesture(cancelled bool) {
208+ g := s.seat.gesture
209+ s.seat.gesture = nil
210+ state := int32(0)
211+ if cancelled {
212+ state = 1
213+ }
214+ s.serial++
215+ for _, o := range g.objs {
216+ if o.alive() {
217+ o.end(s.serial, wireMsec(g.at), state)
218+ }
219+ }
220+}
A internal/adapters/wayland/gestures_test.go +180 -0
@@ -0,0 +1,180 @@
1+package wayland
2+
3+import (
4+ "slices"
5+ "testing"
6+
7+ "github.com/bnema/go-wayland-bindings/server/pointergestures"
8+ "github.com/bnema/go-wayland-bindings/server/wayland"
9+ "github.com/bnema/neferwl/internal/ports"
10+ "github.com/bnema/wlturbo"
11+)
12+
13+// gestureLog records the events of a zwp_pointer_gesture_*_v1 object as
14+// floats: the opcode, then its arguments (fixed values as they are).
15+type gestureLog struct {
16+ wlturbo.BaseProxy
17+ kind ports.GestureKind
18+ events chan []float64
19+}
20+
21+func (p *gestureLog) Dispatch(e *wlturbo.Event) {
22+ ev := []float64{float64(e.Opcode)}
23+ // Hold has no update: its end is opcode 1.
24+ op := e.Opcode
25+ if p.kind == ports.GestureHold && op == 1 {
26+ op = uint16(pointergestures.ZwpPointerGestureSwipeV1EventEnd)
27+ }
28+ switch op {
29+ case uint16(pointergestures.ZwpPointerGestureSwipeV1EventBegin):
30+ // serial, time, surface, fingers
31+ for range 4 {
32+ ev = append(ev, float64(e.Uint32()))
33+ }
34+ case uint16(pointergestures.ZwpPointerGestureSwipeV1EventUpdate):
35+ // time, then dx and dy (and scale and rotation of a pinch)
36+ fixed := 2
37+ if p.kind == ports.GesturePinch {
38+ fixed = 4
39+ }
40+ ev = append(ev, float64(e.Uint32()))
41+ for range fixed {
42+ ev = append(ev, e.Fixed().Float64())
43+ }
44+ case uint16(pointergestures.ZwpPointerGestureSwipeV1EventEnd):
45+ ev = append(ev, float64(e.Uint32()), float64(e.Uint32()), float64(e.Int32()))
46+ }
47+ p.events <- ev
48+}
49+
50+func gestureObjects(t *testing.T, c *wlturbo.Display, manager uint32) (swipe, pinch, hold chan []float64) {
51+ t.Helper()
52+ seat := bindProtocol(t, c, "wl_seat")
53+ registerProtocol(t, c, seat)
54+ pointer := c.AllocateID()
55+ registerProtocol(t, c, pointer)
56+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
57+ var logs [3]chan []float64
58+ for i, op := range []uint32{pointergestures.ZwpPointerGesturesV1RequestGetSwipeGesture, pointergestures.ZwpPointerGesturesV1RequestGetPinchGesture, pointergestures.ZwpPointerGesturesV1RequestGetHoldGesture} {
59+ id := c.AllocateID()
60+ p := &gestureLog{kind: ports.GestureKind(i), events: make(chan []float64, 16)}
61+ p.SetID(id)
62+ registerWireProxy(c, p)
63+ requestProtocol(t, c, manager, op, id, pointer)
64+ logs[i] = p.events
65+ }
66+ return logs[0], logs[1], logs[2]
67+}
68+
69+// quiet fails when the log got an event after a roundtrip.
70+func quiet(t *testing.T, c *wlturbo.Display, events chan []float64) {
71+ t.Helper()
72+ if err := c.Roundtrip(); err != nil {
73+ t.Fatal(err)
74+ }
75+ select {
76+ case ev := <-events:
77+ t.Fatalf("unexpected event %v", ev)
78+ default:
79+ }
80+}
81+
82+func TestPointerGestures(t *testing.T) {
83+ s, events, commands, dir := lifecycleServer(t)
84+ c := protocolClient(t, s, dir)
85+ manager := bindVersion(t, c, "zwp_pointer_gestures_v1", 3)
86+ swipe, pinch, hold := gestureObjects(t, c, manager)
87+ w, surf, _ := surfaceMapper(t, c, events)()
88+ // Another client with gestures never has the pointer.
89+ other := protocolClient(t, s, dir)
90+ oSwipe, oPinch, oHold := gestureObjects(t, other, bindVersion(t, other, "zwp_pointer_gestures_v1", 3))
91+
92+ // No gesture reaches a client before it has the pointer.
93+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2}
94+ commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 10}
95+ quiet(t, c, pinch)
96+
97+ // A pinch: begin carries the serial, time, surface and fingers; updates
98+ // the centre, scale and rotation; the end a later serial.
99+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2, Time: 1000 * 1e6}
100+ begin := next(t, c, pinch)
101+ if begin[0] != 0 || begin[2] != 1000 || begin[3] != float64(surf) || begin[4] != 2 {
102+ t.Fatalf("pinch begin %v", begin)
103+ }
104+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GesturePinch, DX: 1.5, DY: -2, Scale: 1.25, Rotation: 10, Time: 1008 * 1e6}
105+ if ev := next(t, c, pinch); !slices.Equal(ev, []float64{1, 1008, 1.5, -2, 1.25, 10}) {
106+ t.Fatalf("pinch update %v", ev)
107+ }
108+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GesturePinch, Time: 1016 * 1e6}
109+ end := next(t, c, pinch)
110+ if end[0] != 2 || end[1] <= begin[1] || end[2] != 1016 || end[3] != 0 {
111+ t.Fatalf("pinch end %v (begin %v)", end, begin)
112+ }
113+
114+ // A swipe and a hold, the hold ended cancelled.
115+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GestureSwipe, Fingers: 5, Time: 2000 * 1e6}
116+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GestureSwipe, DX: 3, DY: 4, Time: 2008 * 1e6}
117+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GestureSwipe, Time: 2016 * 1e6}
118+ if ev := next(t, c, swipe); ev[0] != 0 || ev[4] != 5 {
119+ t.Fatalf("swipe begin %v", ev)
120+ }
121+ if ev := next(t, c, swipe); !slices.Equal(ev, []float64{1, 2008, 3, 4}) {
122+ t.Fatalf("swipe update %v", ev)
123+ }
124+ if ev := next(t, c, swipe); ev[0] != 2 || ev[3] != 0 {
125+ t.Fatalf("swipe end %v", ev)
126+ }
127+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GestureHold, Fingers: 3, Time: 3000 * 1e6}
128+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GestureHold, Cancelled: true, Time: 3008 * 1e6}
129+ if ev := next(t, c, hold); ev[0] != 0 || ev[4] != 3 {
130+ t.Fatalf("hold begin %v", ev)
131+ }
132+ if ev := next(t, c, hold); ev[0] != 1 || ev[3] != 1 {
133+ t.Fatalf("hold end %v", ev)
134+ }
135+
136+ // A gesture of one kind does not reach the objects of the others, and an
137+ // update of a gesture that is not running is dropped.
138+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GesturePinch, Scale: 2}
139+ quiet(t, c, pinch)
140+ quiet(t, c, swipe)
141+ quiet(t, c, hold)
142+
143+ // The pointer leaving the window cancels the gesture.
144+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2, Time: 4000 * 1e6}
145+ next(t, c, pinch)
146+ commands <- ports.PointerFocus{}
147+ if ev := next(t, c, pinch); ev[0] != 2 || ev[3] != 1 {
148+ t.Fatalf("pinch not cancelled on leave: %v", ev)
149+ }
150+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GesturePinch, Time: 4008 * 1e6}
151+ quiet(t, c, pinch)
152+
153+ // Releasing the manager keeps its objects.
154+ commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 10}
155+ requestProtocol(t, c, manager, pointergestures.ZwpPointerGesturesV1RequestRelease)
156+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GestureHold, Fingers: 4, Time: 5000 * 1e6}
157+ if ev := next(t, c, hold); ev[0] != 0 || ev[4] != 4 {
158+ t.Fatalf("hold begin after release %v", ev)
159+ }
160+
161+ // The client without the pointer saw nothing.
162+ quiet(t, other, oSwipe)
163+ quiet(t, other, oPinch)
164+ quiet(t, other, oHold)
165+}
166+
167+// A window with the pointer but no gesture object gets nothing, and a
168+// gesture on a window that does not have the pointer is refused.
169+func TestPointerGesturesWithoutObjects(t *testing.T) {
170+ s, events, commands, dir := lifecycleServer(t)
171+ c := protocolClient(t, s, dir)
172+ w, _, _ := surfaceMapper(t, c, events)()
173+ commands <- ports.PointerFocus{ID: w.ID, X: 1, Y: 1}
174+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2}
175+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GesturePinch, Scale: 2}
176+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GesturePinch}
177+ if err := c.Roundtrip(); err != nil {
178+ t.Fatal(err)
179+ }
180+}
@@ -0,0 +1,180 @@
1+package wayland
2+
3+import (
4+ "slices"
5+ "testing"
6+
7+ "github.com/bnema/go-wayland-bindings/server/pointergestures"
8+ "github.com/bnema/go-wayland-bindings/server/wayland"
9+ "github.com/bnema/neferwl/internal/ports"
10+ "github.com/bnema/wlturbo"
11+)
12+
13+// gestureLog records the events of a zwp_pointer_gesture_*_v1 object as
14+// floats: the opcode, then its arguments (fixed values as they are).
15+type gestureLog struct {
16+ wlturbo.BaseProxy
17+ kind ports.GestureKind
18+ events chan []float64
19+}
20+
21+func (p *gestureLog) Dispatch(e *wlturbo.Event) {
22+ ev := []float64{float64(e.Opcode)}
23+ // Hold has no update: its end is opcode 1.
24+ op := e.Opcode
25+ if p.kind == ports.GestureHold && op == 1 {
26+ op = uint16(pointergestures.ZwpPointerGestureSwipeV1EventEnd)
27+ }
28+ switch op {
29+ case uint16(pointergestures.ZwpPointerGestureSwipeV1EventBegin):
30+ // serial, time, surface, fingers
31+ for range 4 {
32+ ev = append(ev, float64(e.Uint32()))
33+ }
34+ case uint16(pointergestures.ZwpPointerGestureSwipeV1EventUpdate):
35+ // time, then dx and dy (and scale and rotation of a pinch)
36+ fixed := 2
37+ if p.kind == ports.GesturePinch {
38+ fixed = 4
39+ }
40+ ev = append(ev, float64(e.Uint32()))
41+ for range fixed {
42+ ev = append(ev, e.Fixed().Float64())
43+ }
44+ case uint16(pointergestures.ZwpPointerGestureSwipeV1EventEnd):
45+ ev = append(ev, float64(e.Uint32()), float64(e.Uint32()), float64(e.Int32()))
46+ }
47+ p.events <- ev
48+}
49+
50+func gestureObjects(t *testing.T, c *wlturbo.Display, manager uint32) (swipe, pinch, hold chan []float64) {
51+ t.Helper()
52+ seat := bindProtocol(t, c, "wl_seat")
53+ registerProtocol(t, c, seat)
54+ pointer := c.AllocateID()
55+ registerProtocol(t, c, pointer)
56+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
57+ var logs [3]chan []float64
58+ for i, op := range []uint32{pointergestures.ZwpPointerGesturesV1RequestGetSwipeGesture, pointergestures.ZwpPointerGesturesV1RequestGetPinchGesture, pointergestures.ZwpPointerGesturesV1RequestGetHoldGesture} {
59+ id := c.AllocateID()
60+ p := &gestureLog{kind: ports.GestureKind(i), events: make(chan []float64, 16)}
61+ p.SetID(id)
62+ registerWireProxy(c, p)
63+ requestProtocol(t, c, manager, op, id, pointer)
64+ logs[i] = p.events
65+ }
66+ return logs[0], logs[1], logs[2]
67+}
68+
69+// quiet fails when the log got an event after a roundtrip.
70+func quiet(t *testing.T, c *wlturbo.Display, events chan []float64) {
71+ t.Helper()
72+ if err := c.Roundtrip(); err != nil {
73+ t.Fatal(err)
74+ }
75+ select {
76+ case ev := <-events:
77+ t.Fatalf("unexpected event %v", ev)
78+ default:
79+ }
80+}
81+
82+func TestPointerGestures(t *testing.T) {
83+ s, events, commands, dir := lifecycleServer(t)
84+ c := protocolClient(t, s, dir)
85+ manager := bindVersion(t, c, "zwp_pointer_gestures_v1", 3)
86+ swipe, pinch, hold := gestureObjects(t, c, manager)
87+ w, surf, _ := surfaceMapper(t, c, events)()
88+ // Another client with gestures never has the pointer.
89+ other := protocolClient(t, s, dir)
90+ oSwipe, oPinch, oHold := gestureObjects(t, other, bindVersion(t, other, "zwp_pointer_gestures_v1", 3))
91+
92+ // No gesture reaches a client before it has the pointer.
93+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2}
94+ commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 10}
95+ quiet(t, c, pinch)
96+
97+ // A pinch: begin carries the serial, time, surface and fingers; updates
98+ // the centre, scale and rotation; the end a later serial.
99+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2, Time: 1000 * 1e6}
100+ begin := next(t, c, pinch)
101+ if begin[0] != 0 || begin[2] != 1000 || begin[3] != float64(surf) || begin[4] != 2 {
102+ t.Fatalf("pinch begin %v", begin)
103+ }
104+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GesturePinch, DX: 1.5, DY: -2, Scale: 1.25, Rotation: 10, Time: 1008 * 1e6}
105+ if ev := next(t, c, pinch); !slices.Equal(ev, []float64{1, 1008, 1.5, -2, 1.25, 10}) {
106+ t.Fatalf("pinch update %v", ev)
107+ }
108+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GesturePinch, Time: 1016 * 1e6}
109+ end := next(t, c, pinch)
110+ if end[0] != 2 || end[1] <= begin[1] || end[2] != 1016 || end[3] != 0 {
111+ t.Fatalf("pinch end %v (begin %v)", end, begin)
112+ }
113+
114+ // A swipe and a hold, the hold ended cancelled.
115+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GestureSwipe, Fingers: 5, Time: 2000 * 1e6}
116+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GestureSwipe, DX: 3, DY: 4, Time: 2008 * 1e6}
117+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GestureSwipe, Time: 2016 * 1e6}
118+ if ev := next(t, c, swipe); ev[0] != 0 || ev[4] != 5 {
119+ t.Fatalf("swipe begin %v", ev)
120+ }
121+ if ev := next(t, c, swipe); !slices.Equal(ev, []float64{1, 2008, 3, 4}) {
122+ t.Fatalf("swipe update %v", ev)
123+ }
124+ if ev := next(t, c, swipe); ev[0] != 2 || ev[3] != 0 {
125+ t.Fatalf("swipe end %v", ev)
126+ }
127+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GestureHold, Fingers: 3, Time: 3000 * 1e6}
128+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GestureHold, Cancelled: true, Time: 3008 * 1e6}
129+ if ev := next(t, c, hold); ev[0] != 0 || ev[4] != 3 {
130+ t.Fatalf("hold begin %v", ev)
131+ }
132+ if ev := next(t, c, hold); ev[0] != 1 || ev[3] != 1 {
133+ t.Fatalf("hold end %v", ev)
134+ }
135+
136+ // A gesture of one kind does not reach the objects of the others, and an
137+ // update of a gesture that is not running is dropped.
138+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GesturePinch, Scale: 2}
139+ quiet(t, c, pinch)
140+ quiet(t, c, swipe)
141+ quiet(t, c, hold)
142+
143+ // The pointer leaving the window cancels the gesture.
144+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2, Time: 4000 * 1e6}
145+ next(t, c, pinch)
146+ commands <- ports.PointerFocus{}
147+ if ev := next(t, c, pinch); ev[0] != 2 || ev[3] != 1 {
148+ t.Fatalf("pinch not cancelled on leave: %v", ev)
149+ }
150+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GesturePinch, Time: 4008 * 1e6}
151+ quiet(t, c, pinch)
152+
153+ // Releasing the manager keeps its objects.
154+ commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 10}
155+ requestProtocol(t, c, manager, pointergestures.ZwpPointerGesturesV1RequestRelease)
156+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GestureHold, Fingers: 4, Time: 5000 * 1e6}
157+ if ev := next(t, c, hold); ev[0] != 0 || ev[4] != 4 {
158+ t.Fatalf("hold begin after release %v", ev)
159+ }
160+
161+ // The client without the pointer saw nothing.
162+ quiet(t, other, oSwipe)
163+ quiet(t, other, oPinch)
164+ quiet(t, other, oHold)
165+}
166+
167+// A window with the pointer but no gesture object gets nothing, and a
168+// gesture on a window that does not have the pointer is refused.
169+func TestPointerGesturesWithoutObjects(t *testing.T) {
170+ s, events, commands, dir := lifecycleServer(t)
171+ c := protocolClient(t, s, dir)
172+ w, _, _ := surfaceMapper(t, c, events)()
173+ commands <- ports.PointerFocus{ID: w.ID, X: 1, Y: 1}
174+ commands <- ports.GestureBeginTo{ID: w.ID, Kind: ports.GesturePinch, Fingers: 2}
175+ commands <- ports.GestureUpdateTo{ID: w.ID, Kind: ports.GesturePinch, Scale: 2}
176+ commands <- ports.GestureEndTo{ID: w.ID, Kind: ports.GesturePinch}
177+ if err := c.Roundtrip(); err != nil {
178+ t.Fatal(err)
179+ }
180+}
M internal/adapters/wayland/globals.go +1 -0
@@ -33,6 +33,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
3333 func() error { return registerDMABuf(d, s, o.DMABuf) },
3434 func() error { return registerSinglePixelBuffer(d, s) },
3535 func() error { return registerPointerConstraints(d, s) },
36+ func() error { return registerPointerGestures(d, s) },
3637 func() error { return registerTearing(d, s) },
3738 func() error { return registerColorManagement(d, s) },
3839 func() error { return registerColorRepresentation(d, s) },
@@ -33,6 +33,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
33 func() error { return registerDMABuf(d, s, o.DMABuf) },33 func() error { return registerDMABuf(d, s, o.DMABuf) },
34 func() error { return registerSinglePixelBuffer(d, s) },34 func() error { return registerSinglePixelBuffer(d, s) },
35 func() error { return registerPointerConstraints(d, s) },35 func() error { return registerPointerConstraints(d, s) },
36+ func() error { return registerPointerGestures(d, s) },
36 func() error { return registerTearing(d, s) },37 func() error { return registerTearing(d, s) },
37 func() error { return registerColorManagement(d, s) },38 func() error { return registerColorManagement(d, s) },
38 func() error { return registerColorRepresentation(d, s) },39 func() error { return registerColorRepresentation(d, s) },
M internal/adapters/wayland/seat.go +17 -1
@@ -23,7 +23,9 @@ type seatState struct {
2323 pointerX, pointerY float64
2424 // enters holds the serial of the enter event each wl_pointer received
2525 // for pointerFocus; a pointer warp must name its pointer's serial.
26- enters map[*server.Resource]uint32
26+ enters map[*server.Resource]uint32
27+ // gesture is the touchpad gesture sent to pointerFocus's client.
28+ gesture *activeGesture
2729 wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis
2830 wheelHeld [2]float64 // axis value held back with it for pre-v8 clients
2931 pointers map[server.Client][]*wayland.Pointer
@@ -64,6 +66,12 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
6466 target = c.ID
6567 case ports.PointerAxisTo:
6668 target = c.ID
69+ case ports.GestureBeginTo:
70+ target = c.ID
71+ case ports.GestureUpdateTo:
72+ target = c.ID
73+ case ports.GestureEndTo:
74+ target = c.ID
6775 case ports.FocusWindow:
6876 target = c.ID
6977 case ports.ForwardKey:
@@ -142,6 +150,12 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
142150 for _, p := range pointers {
143151 s.sendAxis(p, c.Axis, steps, values)
144152 }
153+ case ports.GestureBeginTo:
154+ s.beginGesture(c)
155+ case ports.GestureUpdateTo:
156+ s.updateGesture(c)
157+ case ports.GestureEndTo:
158+ s.endGesture(c)
145159 case ports.SetKeymap:
146160 s.setKeymap(c)
147161 case ports.FocusWindow:
@@ -486,6 +500,8 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
486500 return
487501 }
488502 s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}
503+ // A gesture does not follow the pointer to another surface.
504+ s.cancelGesture()
489505 // The new client sets its own cursor on enter; until then, the arrow.
490506 s.cursorSurface = nil
491507 s.setCursor(ports.CursorChange{})
@@ -23,7 +23,9 @@ type seatState struct {
23 pointerX, pointerY float6423 pointerX, pointerY float64
24 // enters holds the serial of the enter event each wl_pointer received24 // enters holds the serial of the enter event each wl_pointer received
25 // for pointerFocus; a pointer warp must name its pointer's serial.25 // for pointerFocus; a pointer warp must name its pointer's serial.
26- enters map[*server.Resource]uint3226+ enters map[*server.Resource]uint32
27+ // gesture is the touchpad gesture sent to pointerFocus's client.
28+ gesture *activeGesture
27 wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis29 wheelRest [2]int32 // v120 not yet sent as axis_discrete, per axis
28 wheelHeld [2]float64 // axis value held back with it for pre-v8 clients30 wheelHeld [2]float64 // axis value held back with it for pre-v8 clients
29 pointers map[server.Client][]*wayland.Pointer31 pointers map[server.Client][]*wayland.Pointer
@@ -64,6 +66,12 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
64 target = c.ID66 target = c.ID
65 case ports.PointerAxisTo:67 case ports.PointerAxisTo:
66 target = c.ID68 target = c.ID
69+ case ports.GestureBeginTo:
70+ target = c.ID
71+ case ports.GestureUpdateTo:
72+ target = c.ID
73+ case ports.GestureEndTo:
74+ target = c.ID
67 case ports.FocusWindow:75 case ports.FocusWindow:
68 target = c.ID76 target = c.ID
69 case ports.ForwardKey:77 case ports.ForwardKey:
@@ -142,6 +150,12 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
142 for _, p := range pointers {150 for _, p := range pointers {
143 s.sendAxis(p, c.Axis, steps, values)151 s.sendAxis(p, c.Axis, steps, values)
144 }152 }
153+ case ports.GestureBeginTo:
154+ s.beginGesture(c)
155+ case ports.GestureUpdateTo:
156+ s.updateGesture(c)
157+ case ports.GestureEndTo:
158+ s.endGesture(c)
145 case ports.SetKeymap:159 case ports.SetKeymap:
146 s.setKeymap(c)160 s.setKeymap(c)
147 case ports.FocusWindow:161 case ports.FocusWindow:
@@ -486,6 +500,8 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
486 return500 return
487 }501 }
488 s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}502 s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}
503+ // A gesture does not follow the pointer to another surface.
504+ s.cancelGesture()
489 // The new client sets its own cursor on enter; until then, the arrow.505 // The new client sets its own cursor on enter; until then, the arrow.
490 s.cursorSurface = nil506 s.cursorSurface = nil
491 s.setCursor(ports.CursorChange{})507 s.setCursor(ports.CursorChange{})
M internal/adapters/wayland/server.go +7 -4
@@ -256,8 +256,10 @@ type Server struct {
256256 clipDevices []*clipDevice
257257 dataSources, primarySources, controlSources map[*server.Resource]*clipSource
258258 // Pointer constraints (constraints.go).
259- regions map[*server.Resource]*region
260- relatives map[server.Client][]*relativepointer.ZwpRelativePointerV1
259+ regions map[*server.Resource]*region
260+ relatives map[server.Client][]*relativepointer.ZwpRelativePointerV1
261+ // gestures are each client's zwp_pointer_gestures_v1 objects (gestures.go).
262+ gestures map[server.Client][]*gestureObject
261263 constraints map[*surface]*constraint
262264 constraint *constraint // the active one
263265 positioners map[*server.Resource]*positioner
@@ -330,6 +332,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
330332 contentNotify: make(chan struct{}),
331333 regions: map[*server.Resource]*region{},
332334 relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{},
335+ gestures: map[server.Client][]*gestureObject{},
333336 constraints: map[*surface]*constraint{},
334337 positioners: map[*server.Resource]*positioner{},
335338 seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), keyStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), pointerStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay},
@@ -737,12 +740,12 @@ func (s *Server) apply(cmd ports.ClientCommand) {
737740 // Only input/focus commands require an owner envelope. Config relay
738741 // also produces SetKeymap and other epoch-independent commands.
739742 switch cmd.(type) {
740- case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.FocusWindow, ports.ForwardKey:
743+ case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey:
741744 return
742745 }
743746 }
744747 switch c := cmd.(type) {
745- case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:
748+ case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:
746749 s.applyInput(c)
747750 case ports.ConfigurePopup:
748751 if w := s.windows[c.ID]; w != nil && w.popup != nil {
@@ -256,8 +256,10 @@ type Server struct {
256 clipDevices []*clipDevice256 clipDevices []*clipDevice
257 dataSources, primarySources, controlSources map[*server.Resource]*clipSource257 dataSources, primarySources, controlSources map[*server.Resource]*clipSource
258 // Pointer constraints (constraints.go).258 // Pointer constraints (constraints.go).
259- regions map[*server.Resource]*region259+ regions map[*server.Resource]*region
260- relatives map[server.Client][]*relativepointer.ZwpRelativePointerV1260+ relatives map[server.Client][]*relativepointer.ZwpRelativePointerV1
261+ // gestures are each client's zwp_pointer_gestures_v1 objects (gestures.go).
262+ gestures map[server.Client][]*gestureObject
261 constraints map[*surface]*constraint263 constraints map[*surface]*constraint
262 constraint *constraint // the active one264 constraint *constraint // the active one
263 positioners map[*server.Resource]*positioner265 positioners map[*server.Resource]*positioner
@@ -330,6 +332,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
330 contentNotify: make(chan struct{}),332 contentNotify: make(chan struct{}),
331 regions: map[*server.Resource]*region{},333 regions: map[*server.Resource]*region{},
332 relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{},334 relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{},
335+ gestures: map[server.Client][]*gestureObject{},
333 constraints: map[*surface]*constraint{},336 constraints: map[*surface]*constraint{},
334 positioners: map[*server.Resource]*positioner{},337 positioners: map[*server.Resource]*positioner{},
335 seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), keyStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), pointerStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay},338 seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), keyStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), pointerStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay},
@@ -737,12 +740,12 @@ func (s *Server) apply(cmd ports.ClientCommand) {
737 // Only input/focus commands require an owner envelope. Config relay740 // Only input/focus commands require an owner envelope. Config relay
738 // also produces SetKeymap and other epoch-independent commands.741 // also produces SetKeymap and other epoch-independent commands.
739 switch cmd.(type) {742 switch cmd.(type) {
740- case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.FocusWindow, ports.ForwardKey:743+ case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey:
741 return744 return
742 }745 }
743 }746 }
744 switch c := cmd.(type) {747 switch c := cmd.(type) {
745- case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:748+ case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:
746 s.applyInput(c)749 s.applyInput(c)
747 case ports.ConfigurePopup:750 case ports.ConfigurePopup:
748 if w := s.windows[c.ID]; w != nil && w.popup != nil {751 if w := s.windows[c.ID]; w != nil && w.popup != nil {
M internal/adapters/wayland/wire_client_test.go +5 -0
@@ -32,6 +32,7 @@ import (
3232 wire_linuxdmabuf "github.com/bnema/go-wayland-bindings/server/linuxdmabuf"
3333 wire_linuxdrmsyncobj "github.com/bnema/go-wayland-bindings/server/linuxdrmsyncobj"
3434 wire_pointerconstraints "github.com/bnema/go-wayland-bindings/server/pointerconstraints"
35+ wire_pointergestures "github.com/bnema/go-wayland-bindings/server/pointergestures"
3536 wire_pointerwarp "github.com/bnema/go-wayland-bindings/server/pointerwarp"
3637 wire_presentationtime "github.com/bnema/go-wayland-bindings/server/presentationtime"
3738 wire_primaryselection "github.com/bnema/go-wayland-bindings/server/primaryselection"
@@ -239,6 +240,10 @@ var wireSchemas = map[string]wireSchema{
239240 "zwp_primary_selection_device_v1": {wire_primaryselection.ZwpPrimarySelectionDeviceV1Interface, &client_primaryselection.PrimarySelectionDevice{}, map[uint16]bool{1: true}, nil},
240241 "zwp_primary_selection_offer_v1": {wire_primaryselection.ZwpPrimarySelectionOfferV1Interface, &client_primaryselection.PrimarySelectionOffer{}, map[uint16]bool{1: true}, nil},
241242 "zwp_primary_selection_source_v1": {wire_primaryselection.ZwpPrimarySelectionSourceV1Interface, &client_primaryselection.PrimarySelectionSource{}, map[uint16]bool{1: true}, nil},
243+ "zwp_pointer_gestures_v1": {wire_pointergestures.ZwpPointerGesturesV1Interface, nil, map[uint16]bool{2: true}, nil},
244+ "zwp_pointer_gesture_swipe_v1": {wire_pointergestures.ZwpPointerGestureSwipeV1Interface, nil, map[uint16]bool{0: true}, nil},
245+ "zwp_pointer_gesture_pinch_v1": {wire_pointergestures.ZwpPointerGesturePinchV1Interface, nil, map[uint16]bool{0: true}, nil},
246+ "zwp_pointer_gesture_hold_v1": {wire_pointergestures.ZwpPointerGestureHoldV1Interface, nil, map[uint16]bool{0: true}, nil},
242247 "zwp_relative_pointer_manager_v1": {wire_relativepointer.ZwpRelativePointerManagerV1Interface, &client_relativepointer.RelativePointerManager{}, map[uint16]bool{0: true}, nil},
243248 "zwp_relative_pointer_v1": {wire_relativepointer.ZwpRelativePointerV1Interface, &client_relativepointer.RelativePointer{}, map[uint16]bool{0: true}, nil},
244249 "zwp_text_input_manager_v3": {wire_textinput.ZwpTextInputManagerV3Interface, &client_textinput.TextInputManagerV3{}, map[uint16]bool{0: true}, nil},
@@ -32,6 +32,7 @@ import (
32 wire_linuxdmabuf "github.com/bnema/go-wayland-bindings/server/linuxdmabuf"32 wire_linuxdmabuf "github.com/bnema/go-wayland-bindings/server/linuxdmabuf"
33 wire_linuxdrmsyncobj "github.com/bnema/go-wayland-bindings/server/linuxdrmsyncobj"33 wire_linuxdrmsyncobj "github.com/bnema/go-wayland-bindings/server/linuxdrmsyncobj"
34 wire_pointerconstraints "github.com/bnema/go-wayland-bindings/server/pointerconstraints"34 wire_pointerconstraints "github.com/bnema/go-wayland-bindings/server/pointerconstraints"
35+ wire_pointergestures "github.com/bnema/go-wayland-bindings/server/pointergestures"
35 wire_pointerwarp "github.com/bnema/go-wayland-bindings/server/pointerwarp"36 wire_pointerwarp "github.com/bnema/go-wayland-bindings/server/pointerwarp"
36 wire_presentationtime "github.com/bnema/go-wayland-bindings/server/presentationtime"37 wire_presentationtime "github.com/bnema/go-wayland-bindings/server/presentationtime"
37 wire_primaryselection "github.com/bnema/go-wayland-bindings/server/primaryselection"38 wire_primaryselection "github.com/bnema/go-wayland-bindings/server/primaryselection"
@@ -239,6 +240,10 @@ var wireSchemas = map[string]wireSchema{
239 "zwp_primary_selection_device_v1": {wire_primaryselection.ZwpPrimarySelectionDeviceV1Interface, &client_primaryselection.PrimarySelectionDevice{}, map[uint16]bool{1: true}, nil},240 "zwp_primary_selection_device_v1": {wire_primaryselection.ZwpPrimarySelectionDeviceV1Interface, &client_primaryselection.PrimarySelectionDevice{}, map[uint16]bool{1: true}, nil},
240 "zwp_primary_selection_offer_v1": {wire_primaryselection.ZwpPrimarySelectionOfferV1Interface, &client_primaryselection.PrimarySelectionOffer{}, map[uint16]bool{1: true}, nil},241 "zwp_primary_selection_offer_v1": {wire_primaryselection.ZwpPrimarySelectionOfferV1Interface, &client_primaryselection.PrimarySelectionOffer{}, map[uint16]bool{1: true}, nil},
241 "zwp_primary_selection_source_v1": {wire_primaryselection.ZwpPrimarySelectionSourceV1Interface, &client_primaryselection.PrimarySelectionSource{}, map[uint16]bool{1: true}, nil},242 "zwp_primary_selection_source_v1": {wire_primaryselection.ZwpPrimarySelectionSourceV1Interface, &client_primaryselection.PrimarySelectionSource{}, map[uint16]bool{1: true}, nil},
243+ "zwp_pointer_gestures_v1": {wire_pointergestures.ZwpPointerGesturesV1Interface, nil, map[uint16]bool{2: true}, nil},
244+ "zwp_pointer_gesture_swipe_v1": {wire_pointergestures.ZwpPointerGestureSwipeV1Interface, nil, map[uint16]bool{0: true}, nil},
245+ "zwp_pointer_gesture_pinch_v1": {wire_pointergestures.ZwpPointerGesturePinchV1Interface, nil, map[uint16]bool{0: true}, nil},
246+ "zwp_pointer_gesture_hold_v1": {wire_pointergestures.ZwpPointerGestureHoldV1Interface, nil, map[uint16]bool{0: true}, nil},
242 "zwp_relative_pointer_manager_v1": {wire_relativepointer.ZwpRelativePointerManagerV1Interface, &client_relativepointer.RelativePointerManager{}, map[uint16]bool{0: true}, nil},247 "zwp_relative_pointer_manager_v1": {wire_relativepointer.ZwpRelativePointerManagerV1Interface, &client_relativepointer.RelativePointerManager{}, map[uint16]bool{0: true}, nil},
243 "zwp_relative_pointer_v1": {wire_relativepointer.ZwpRelativePointerV1Interface, &client_relativepointer.RelativePointer{}, map[uint16]bool{0: true}, nil},248 "zwp_relative_pointer_v1": {wire_relativepointer.ZwpRelativePointerV1Interface, &client_relativepointer.RelativePointer{}, map[uint16]bool{0: true}, nil},
244 "zwp_text_input_manager_v3": {wire_textinput.ZwpTextInputManagerV3Interface, &client_textinput.TextInputManagerV3{}, map[uint16]bool{0: true}, nil},249 "zwp_text_input_manager_v3": {wire_textinput.ZwpTextInputManagerV3Interface, &client_textinput.TextInputManagerV3{}, map[uint16]bool{0: true}, nil},

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 25

Tags 10

↑↓ moveenter openesc close