1package wayland
2
3import (
4 "context"
5
6 "github.com/bnema/neferwl/internal/ports"
7 "github.com/bnema/purego-libwayland/protocol/cursorshape"
8 "github.com/bnema/purego-libwayland/protocol/tabletv2"
9 "github.com/bnema/purego-libwayland/protocol/wayland"
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, *tabletv2.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.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 var state *surface
74 if surf != nil {
75 state = s.surfaces[surf.Resource]
76 if state == nil {
77 return
78 }
79 if state.kind != roleNone && state.kind != roleCursor {
80 r.PostError(uint32(wayland.PointerErrorRole), "surface already has another role")
81 return
82 }
83 state.kind = roleCursor
84 state.role = func(bool) {
85 if s.cursorSurface == state {
86 s.setCursor(state.cursorImage())
87 }
88 }
89 }
90 if !s.hasPointerFocus(r.Client()) {
91 return
92 }
93 if state == nil {
94 s.cursorSurface = nil
95 s.setCursor(ports.CursorChange{Hidden: true})
96 return
97 }
98 s.cursorSurface, state.hotX, state.hotY = state, int(hotX), int(hotY)
99 s.setCursor(state.cursorImage())
100}
101func (pointer) Release(r *wayland.Pointer) { r.Destroy() }
102
103// hasPointerFocus reports whether the client owns the window or layer
104// under the pointer.
105func (s *Server) hasPointerFocus(c server.Client) bool {
106 owner, _, ok := s.pointerTarget(s.pointerFocus)
107 return ok && owner == c
108}
109
110// cursorImage copies the committed shm buffer of a cursor surface. No
111// buffer hides the cursor; an unreadable one falls back to the arrow.
112func (s *surface) cursorImage() ports.CursorChange {
113 if s.current == nil {
114 return ports.CursorChange{Hidden: true}
115 }
116 b, ok := s.server.buffers[s.current.Resource].(*buffer)
117 if !ok || b.width > maxCursorSide || b.height > maxCursorSide {
118 return ports.CursorChange{}
119 }
120 img := ports.CursorImage{W: b.width, H: b.height, Pixels: make([]byte, b.width*b.height*4)}
121 for y := range b.height {
122 row := img.Pixels[y*b.width*4 : (y+1)*b.width*4]
123 if _, err := b.pool.file.ReadAt(row, int64(b.offset+y*b.stride)); err != nil {
124 return ports.CursorChange{}
125 }
126 if b.format == uint32(wayland.ShmFormatXrgb8888) {
127 for i := 3; i < len(row); i += 4 {
128 row[i] = 255
129 }
130 }
131 }
132 scale := max(s.bufferScale, 1)
133 img.HotX, img.HotY = min(max(s.hotX*scale, 0), b.width-1), min(max(s.hotY*scale, 0), b.height-1)
134 return ports.CursorChange{Image: &img, Scale: scale}
135}
136
137// setCursor queues a cursor change; only the latest is delivered.
138func (s *Server) setCursor(c ports.CursorChange) {
139 if s.channels.Cursors == nil {
140 return
141 }
142 s.log.Debug().Str("shape", c.Shape).Bool("client", c.Image != nil).Bool("hidden", c.Hidden).Msg("cursor")
143 s.cursorMu.Lock()
144 s.cursorLatest, s.cursorQueued = c, true
145 s.cursorMu.Unlock()
146 select {
147 case s.cursorReady <- struct{}{}:
148 default:
149 }
150}
151
152func (s *Server) forwardCursors(ctx context.Context) {
153 for {
154 select {
155 case <-ctx.Done():
156 return
157 case <-s.cursorReady:
158 }
159 s.cursorMu.Lock()
160 c, ok := s.cursorLatest, s.cursorQueued
161 s.cursorQueued = false
162 s.cursorMu.Unlock()
163 if !ok {
164 continue
165 }
166 select {
167 case <-ctx.Done():
168 return
169 case s.channels.Cursors <- c:
170 }
171 }
172}