v0.1.0 d9218d6

constraints.go RawHistoryFolder

1package wayland
2
3import (
4	"github.com/bnema/neferwl/internal/ports"
5	"github.com/bnema/purego-libwayland/protocol/pointerconstraints"
6	"github.com/bnema/purego-libwayland/protocol/relativepointer"
7	"github.com/bnema/purego-libwayland/protocol/wayland"
8	"github.com/bnema/purego-libwayland/server"
9)
10
11func registerPointerConstraints(d *server.Display, s *Server) error {
12	if err := relativepointer.NewZwpRelativePointerManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
13		_, _ = relativepointer.NewZwpRelativePointerManagerV1(c, int32(v), id, relativeManager{s})
14	}); err != nil {
15		return err
16	}
17	return pointerconstraints.NewZwpPointerConstraintsV1Global(d, 1, func(c server.Client, v, id uint32) {
18		_, _ = pointerconstraints.NewZwpPointerConstraintsV1(c, int32(v), id, constraintManager{s})
19	})
20}
21
22type relativeManager struct{ server *Server }
23
24func (relativeManager) Destroy(*relativepointer.ZwpRelativePointerManagerV1) {}
25func (m relativeManager) GetRelativePointer(r *relativepointer.ZwpRelativePointerManagerV1, id uint32, _ *wayland.Pointer) {
26	rel, err := relativepointer.NewZwpRelativePointerV1(r.Client(), r.Version(), id, relativeHandler{})
27	if err != nil {
28		return
29	}
30	s, c := m.server, r.Client()
31	s.relatives[c] = append(s.relatives[c], rel)
32	rel.OnDestroy = func() {
33		if list := removeItem(s.relatives[c], rel); len(list) > 0 {
34			s.relatives[c] = list
35		} else {
36			delete(s.relatives, c)
37		}
38	}
39}
40
41type relativeHandler struct{}
42
43func (relativeHandler) Destroy(*relativepointer.ZwpRelativePointerV1) {}
44
45// relativeMotion sends the motion deltas to the window client's relative
46// pointers. Absolute devices have no deltas and send nothing.
47// It reports whether any event was sent.
48func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) bool {
49	if c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 {
50		return false
51	}
52	if !w.mapped || !w.xdg.resource.Resource.Alive() {
53		return false
54	}
55	sent := false
56	for _, rel := range s.relatives[w.xdg.resource.Client()] {
57		if rel.Resource.Alive() {
58			rel.SendRelativeMotion(uint32(c.TimeUsec>>32), uint32(c.TimeUsec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY))
59			sent = true
60		}
61	}
62	return sent
63}
64
65// constraint is a zwp_locked_pointer_v1 or zwp_confined_pointer_v1 on a
66// surface. Its region is surface-local; empty means the whole surface.
67type constraint struct {
68	surface    *surface
69	lock       *pointerconstraints.ZwpLockedPointerV1
70	confine    *pointerconstraints.ZwpConfinedPointerV1
71	region     ports.Rect
72	pending    *ports.Rect // set_region waiting for the surface commit
73	persistent bool
74	active     bool
75	defunct    bool // a oneshot constraint that was deactivated
76}
77
78type constraintManager struct{ server *Server }
79
80func (constraintManager) Destroy(*pointerconstraints.ZwpPointerConstraintsV1) {}
81func (m constraintManager) LockPointer(r *pointerconstraints.ZwpPointerConstraintsV1, id uint32, surf *wayland.Surface, _ *wayland.Pointer, reg *wayland.Region, lifetime uint32) {
82	c := m.create(r, surf, reg, lifetime)
83	if c == nil {
84		// An unknown surface: an inert object the client can destroy.
85		_, _ = pointerconstraints.NewZwpLockedPointerV1(r.Client(), r.Version(), id, lockHandler{&constraint{}})
86		return
87	}
88	lock, err := pointerconstraints.NewZwpLockedPointerV1(r.Client(), r.Version(), id, lockHandler{c})
89	if err != nil {
90		delete(m.server.constraints, c.surface)
91		return
92	}
93	c.lock = lock
94	lock.OnDestroy = func() { m.server.dropConstraint(c) }
95	m.server.updateConstraint()
96}
97func (m constraintManager) ConfinePointer(r *pointerconstraints.ZwpPointerConstraintsV1, id uint32, surf *wayland.Surface, _ *wayland.Pointer, reg *wayland.Region, lifetime uint32) {
98	c := m.create(r, surf, reg, lifetime)
99	if c == nil {
100		_, _ = pointerconstraints.NewZwpConfinedPointerV1(r.Client(), r.Version(), id, confineHandler{&constraint{}})
101		return
102	}
103	confine, err := pointerconstraints.NewZwpConfinedPointerV1(r.Client(), r.Version(), id, confineHandler{c})
104	if err != nil {
105		delete(m.server.constraints, c.surface)
106		return
107	}
108	c.confine = confine
109	confine.OnDestroy = func() { m.server.dropConstraint(c) }
110	m.server.updateConstraint()
111}
112
113// create validates the request and registers the constraint on the surface.
114func (m constraintManager) create(r *pointerconstraints.ZwpPointerConstraintsV1, surf *wayland.Surface, reg *wayland.Region, lifetime uint32) *constraint {
115	s := m.server
116	if surf == nil {
117		return nil
118	}
119	state := s.surfaces[surf.Resource]
120	if state == nil {
121		return nil
122	}
123	if s.constraints[state] != nil {
124		r.PostError(uint32(pointerconstraints.ZwpPointerConstraintsV1ErrorAlreadyConstrained), "surface already constrained")
125		return nil
126	}
127	if state.kind == roleCursor {
128		return nil
129	}
130	c := &constraint{surface: state, region: s.regionBox(reg), persistent: lifetime == uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)}
131	s.constraints[state] = c
132	return c
133}
134
135// regionBox copies a region: the protocol reads it at request time.
136func (s *Server) regionBox(reg *wayland.Region) ports.Rect {
137	if reg == nil || reg.Resource == nil {
138		return ports.Rect{}
139	}
140	if g := s.regions[reg.Resource]; g != nil {
141		return g.box
142	}
143	return ports.Rect{}
144}
145
146type lockHandler struct{ c *constraint }
147
148func (lockHandler) Destroy(*pointerconstraints.ZwpLockedPointerV1) {}
149
150// SetCursorPositionHint is ignored: the pointer stays where it locked.
151func (lockHandler) SetCursorPositionHint(*pointerconstraints.ZwpLockedPointerV1, server.Fixed, server.Fixed) {
152}
153func (h lockHandler) SetRegion(_ *pointerconstraints.ZwpLockedPointerV1, reg *wayland.Region) {
154	if h.c.surface == nil {
155		return
156	}
157	r := h.c.surface.server.regionBox(reg)
158	h.c.pending = &r
159}
160
161type confineHandler struct{ c *constraint }
162
163func (confineHandler) Destroy(*pointerconstraints.ZwpConfinedPointerV1) {}
164func (h confineHandler) SetRegion(_ *pointerconstraints.ZwpConfinedPointerV1, reg *wayland.Region) {
165	if h.c.surface == nil {
166		return
167	}
168	r := h.c.surface.server.regionBox(reg)
169	h.c.pending = &r
170}
171
172// commitConstraint applies a pending region on the surface commit, after
173// the new window geometry: core receives the region window-local.
174func (s *surface) commitConstraint(geometry bool) {
175	c := s.server.constraints[s]
176	if c == nil || (c.pending == nil && !geometry) {
177		return
178	}
179	if c.pending != nil {
180		c.region, c.pending = *c.pending, nil
181	}
182	if c.active {
183		s.server.emitConstraint(c)
184	} else {
185		s.server.updateConstraint()
186	}
187}
188
189// dropConstraint forgets a destroyed constraint or one on a destroyed surface.
190func (s *Server) dropConstraint(c *constraint) {
191	if s.constraints[c.surface] == c {
192		delete(s.constraints, c.surface)
193	}
194	if s.constraint == c {
195		c.active = false
196		s.constraint = nil
197		if c.lock != nil && c.lock.Resource.Alive() {
198			c.lock.SendUnlocked()
199		}
200		if c.confine != nil && c.confine.Resource.Alive() {
201			c.confine.SendUnconfined()
202		}
203		s.emit(ports.PointerConstrained{})
204	}
205	s.updateConstraint()
206}
207
208// updateConstraint activates the constraint of the window holding both
209// pointer and keyboard focus, and deactivates any other.
210func (s *Server) updateConstraint() {
211	var want *constraint
212	if w := s.windows[s.pointerFocus]; w != nil && w.mapped && s.pointerFocus == s.focused {
213		if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct && (c.active || c.contains(w)) {
214			want = c
215		}
216	}
217	if want == s.constraint {
218		return
219	}
220	if old := s.constraint; old != nil {
221		old.active = false
222		if !old.persistent {
223			old.defunct = true
224		}
225		if old.lock != nil && old.lock.Resource.Alive() {
226			old.lock.SendUnlocked()
227		}
228		if old.confine != nil && old.confine.Resource.Alive() {
229			old.confine.SendUnconfined()
230		}
231	}
232	s.constraint = want
233	if want == nil {
234		s.emit(ports.PointerConstrained{})
235		return
236	}
237	want.active = true
238	if want.lock != nil {
239		want.lock.SendLocked()
240	} else {
241		want.confine.SendConfined()
242	}
243	s.emitConstraint(want)
244}
245
246// emitConstraint tells core about the active constraint, window-local.
247func (s *Server) emitConstraint(c *constraint) {
248	x := c.surface.xdg
249	if x == nil || x.window == nil {
250		return
251	}
252	mode := ports.ConstraintConfine
253	if c.lock != nil {
254		mode = ports.ConstraintLock
255	}
256	r := c.region
257	if r.W > 0 && r.H > 0 {
258		r.X -= x.geometry.X
259		r.Y -= x.geometry.Y
260	}
261	s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}})
262}
263
264// contains reports whether the pointer is in the region: a constraint
265// activates only there, so it never warps the pointer.
266func (c *constraint) contains(w *window) bool {
267	r := c.region
268	if r.W <= 0 || r.H <= 0 {
269		return true
270	}
271	x, y := w.surfacePoint(w.xdg.server.pointerX, w.xdg.server.pointerY)
272	return x >= float64(r.X) && x < float64(r.X+r.W) && y >= float64(r.Y) && y < float64(r.Y+r.H)
273}
274
275// locked reports whether the window's pointer is locked.
276func (s *Server) locked(w *window) bool {
277	return s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.xdg.surface
278}

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