1package wayland
2
3import (
4 "context"
5
6 "github.com/bnema/go-wayland-bindings/server/cursorshape"
7 "github.com/bnema/go-wayland-bindings/server/tablet"
8 "github.com/bnema/go-wayland-bindings/server/wayland"
9 "github.com/bnema/neferwl/internal/ports"
10 "github.com/bnema/purego-libwayland/server"
11)
12
13// The client under the pointer picks the cursor: a theme shape
14// (wp_cursor_shape_v1), its own surface (wl_pointer.set_cursor) or none.
15// Changes go to the outputs latest first; a pointer focus change resets
16// the cursor to the arrow until the new client sets one.
17
18// maxCursorSide bounds client cursor images read from shm.
19const maxCursorSide = 256
20
21// cursorShapes are the CSS names of wp_cursor_shape_device_v1 shapes, by
22// enum value.
23var cursorShapes = [...]string{
24 1: "default", "context-menu", "help", "pointer", "progress", "wait", "cell",
25 "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop",
26 "not-allowed", "grab", "grabbing", "e-resize", "n-resize", "ne-resize",
27 "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize",
28 "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize",
29 "all-scroll", "zoom-in", "zoom-out", "dnd-ask", "all-resize",
30}
31
32func registerCursorShape(d *server.Display, s *Server) error {
33 return cursorshape.NewWpCursorShapeManagerV1Global(d, 2, func(c server.Client, v, id uint32) {
34 _, _ = cursorshape.NewWpCursorShapeManagerV1(c, int32(v), id, cursorShapeManager{s})
35 })
36}
37
38type cursorShapeManager struct{ server *Server }
39
40func (cursorShapeManager) Destroy(r *cursorshape.WpCursorShapeManagerV1) { r.Destroy() }
41func (m cursorShapeManager) GetPointer(r *cursorshape.WpCursorShapeManagerV1, id uint32, _ *wayland.Pointer) {
42 _, _ = cursorshape.NewWpCursorShapeDeviceV1(r.Client(), r.Version(), id, cursorShapeDevice(m))
43}
44
45// GetTabletToolV2 is unreachable: neferwl has no tablet global.
46func (cursorShapeManager) GetTabletToolV2(*cursorshape.WpCursorShapeManagerV1, uint32, *tablet.ZwpTabletToolV2) {
47}
48
49type cursorShapeDevice struct{ server *Server }
50
51func (cursorShapeDevice) Destroy(r *cursorshape.WpCursorShapeDeviceV1) { r.Destroy() }
52func (d cursorShapeDevice) SetShape(r *cursorshape.WpCursorShapeDeviceV1, _ uint32, shape uint32) {
53 last := cursorshape.WpCursorShapeDeviceV1ShapeZoomOut
54 if r.Version() >= 2 {
55 last = cursorshape.WpCursorShapeDeviceV1ShapeAllResize
56 }
57 if shape < uint32(cursorshape.WpCursorShapeDeviceV1ShapeDefault) || shape > uint32(last) {
58 r.PostError(uint32(cursorshape.WpCursorShapeDeviceV1ErrorInvalidShape), "invalid shape")
59 return
60 }
61 s := d.server
62 if s.protected() || !s.hasPointerFocus(r.Client()) {
63 return
64 }
65 s.cursorSurface = nil
66 s.setCursor(ports.CursorChange{Shape: cursorShapes[shape]})
67}
68
69type pointer struct{ server *Server }
70
71func (p pointer) SetCursor(r *wayland.Pointer, _ uint32, surf *wayland.Surface, hotX, hotY int32) {
72 s := p.server
73 if s.protected() {
74 return
75 }
76 var state *surface
77 if surf != nil {
78 state = s.surfaces[surf.Resource]
79 if state == nil {
80 return
81 }
82 if state.kind != roleNone && state.kind != roleCursor {
83 r.PostError(uint32(wayland.PointerErrorRole), "surface already has another role")
84 return
85 }
86 state.kind = roleCursor
87 state.role = func(bool) {
88 if s.cursorSurface == state {
89 s.setCursor(state.cursorImage())
90 }
91 }
92 }
93 if !s.hasPointerFocus(r.Client()) {
94 return
95 }
96 if state == nil {
97 s.cursorSurface = nil
98 s.setCursor(ports.CursorChange{Hidden: true})
99 return
100 }
101 s.cursorSurface, state.hotX, state.hotY = state, int(hotX), int(hotY)
102 s.setCursor(state.cursorImage())
103}
104func (pointer) Release(r *wayland.Pointer) { r.Destroy() }
105
106// hasPointerFocus reports whether the client owns the window or layer
107// under the pointer.
108func (s *Server) hasPointerFocus(c server.Client) bool {
109 owner, _, ok := s.pointerTarget(s.seat.pointerFocus)
110 return ok && owner == c
111}
112
113// cursorImage copies the committed shm buffer of a cursor surface. No
114// buffer hides the cursor; an unreadable or transformed one falls back to
115// the arrow.
116func (s *surface) cursorImage() ports.CursorChange {
117 if s.current == nil {
118 return ports.CursorChange{Hidden: true}
119 }
120 b, ok := s.server.buffers[s.current.Resource].(*buffer)
121 if !ok || b.width > maxCursorSide || b.height > maxCursorSide || s.transform != 0 {
122 return ports.CursorChange{}
123 }
124 img := ports.CursorImage{W: b.width, H: b.height, Pixels: make([]byte, b.width*b.height*4)}
125 for y := range b.height {
126 row := img.Pixels[y*b.width*4 : (y+1)*b.width*4]
127 if _, err := b.pool.file.ReadAt(row, int64(b.offset+y*b.stride)); err != nil {
128 return ports.CursorChange{}
129 }
130 if b.format == uint32(wayland.ShmFormatXrgb8888) {
131 for i := 3; i < len(row); i += 4 {
132 row[i] = 255
133 }
134 }
135 }
136 scale := max(s.bufferScale, 1)
137 img.HotX, img.HotY = min(max(s.hotX*scale, 0), b.width-1), min(max(s.hotY*scale, 0), b.height-1)
138 return ports.CursorChange{Image: &img, Scale: scale}
139}
140
141// setCursor queues a cursor change; only the latest is delivered.
142func (s *Server) setCursor(c ports.CursorChange) {
143 if s.protected() || s.channels.Cursors == nil {
144 return
145 }
146 s.cursorMu.Lock()
147 if c.Image == nil && (c.Shape != s.cursorLatest.Shape || c.Hidden != s.cursorLatest.Hidden || s.cursorLatest.Image != nil) {
148 s.log.Debug().Str("shape", c.Shape).Bool("hidden", c.Hidden).Msg("cursor")
149 }
150 s.cursorLatest, s.cursorQueued = c, true
151 s.cursorMu.Unlock()
152 select {
153 case s.cursorReady <- struct{}{}:
154 default:
155 }
156}
157
158func (s *Server) forwardCursors(ctx context.Context) {
159 for {
160 select {
161 case <-ctx.Done():
162 return
163 case <-s.cursorReady:
164 }
165 s.cursorMu.Lock()
166 c, ok := s.cursorLatest, s.cursorQueued
167 s.cursorQueued = false
168 s.cursorMu.Unlock()
169 if !ok || s.protected() {
170 continue
171 }
172 select {
173 case <-ctx.Done():
174 return
175 case s.channels.Cursors <- c:
176 }
177 }
178}