v0.1.0 d9218d6

cursor.go RawHistoryFolder

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}

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 23

Tags 10

↑↓ moveenter openesc close