v0.4.0 b6be3a8

cursor.go RawHistoryFolder

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}

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