v0.1.0 d9218d6

surface.go RawHistoryFolder

1package wayland
2
3import (
4	"github.com/bnema/purego-libwayland/protocol/presentationtime"
5	"image"
6	"time"
7
8	"github.com/bnema/neferwl/internal/ports"
9	"github.com/bnema/purego-libwayland/protocol/wayland"
10	"github.com/bnema/purego-libwayland/protocol/wlrlayershell"
11	"github.com/bnema/purego-libwayland/protocol/xdgshell"
12)
13
14type roleKind uint8
15
16const (
17	roleNone roleKind = iota
18	roleXDG
19	roleSubsurface
20	roleLayer
21	roleCursor
22)
23
24type surface struct {
25	wl       *wayland.Surface
26	viewport *viewport
27	// bufferScale is committed state; pendingScale is set by set_buffer_scale.
28	bufferScale, pendingScale int
29	kind                      roleKind
30	xdg                       *xdgSurface
31	layer                     *layerSurface
32	server                    *Server
33	current, pending          *wayland.Buffer
34	attached                  bool
35	lastW, lastH              int // last logged buffer size
36	callbacks                 []*wayland.Callback
37	role                      func(bool)
38	destroyed                 bool
39	// hotX, hotY is the click point of a cursor surface (logical).
40	hotX, hotY int
41	// on is the output the surface entered; scale is the last scale sent.
42	on    *output
43	scale float64
44	// content is the last content built from this surface's own buffer;
45	// has is false while no buffer is attached.
46	content ports.SurfaceContent
47	has     bool
48	sub     subState
49	// tearing is the surface's wp_tearing_control_v1; async is the
50	// committed hint, pendingAsync the requested one.
51	tearing             *tearingHandler
52	async, pendingAsync bool
53	// Presentation constraints (fifo.go): pending fifo requests and commit
54	// timestamp, the barrier, and the commits waiting to apply.
55	fifo                        *fifoHandler
56	timer                       *timerHandler
57	pendingBarrier, pendingWait bool
58	pendingTime                 time.Time
59	barrier                     bool
60	barrierAt                   time.Time
61	queue                       []update
62	// contentType is the wp_content_type_v1; contentKind the committed type.
63	contentType              *contentTypeHandler
64	contentKind, pendingKind uint32
65	// Damage requested since the last commit: in surface (logical) and
66	// buffer pixels; committed is what the last commit changed, in buffer
67	// pixels (full: everything), read by the window's damage history.
68	pendingDamage, pendingBufDamage []ports.Rect
69	committed                       damage
70	// pendingFeedback are wp_presentation feedbacks for the next commit.
71	pendingFeedback []*presentationtime.WpPresentationFeedback
72	// Explicit sync (syncobj.go): the surface's syncobj object; the
73	// committed points of the pending commit (pendingSync, with the wait
74	// for its acquire point); and the current buffer's hold.
75	sync        *syncState
76	pendingSync *commitSync
77	hold        syncHold
78}
79
80// commitSync is a commit's explicit-sync points and acquire wait.
81type commitSync struct {
82	acquire, release syncPoint
83	wait             *syncWait
84}
85
86// damage is what one commit changed in a surface's buffer.
87type damage struct {
88	full  bool
89	rects []ports.Rect
90}
91
92// maxDamageRects bounds one commit's rects: past it the commit is full.
93const maxDamageRects = 32
94
95// subState is the subsurface tree: parent is set on subsurfaces, children
96// are ordered bottom to top, below marks children under their parent.
97type subState struct {
98	// role is the live wl_subsurface, nil when the surface has none.
99	role         *wayland.Subsurface
100	parent       *surface
101	children     []*surface
102	x, y         int // committed position, from the parent origin
103	pendX, pendY int
104	moved        bool // a position waits for the parent commit
105	below        bool
106}
107
108// root is the top of the surface's subsurface tree.
109func (s *surface) root() *surface {
110	for s.sub.parent != nil {
111		s = s.sub.parent
112	}
113	return s
114}
115
116// windowID is the mapped window or layer the surface draws, or 0.
117func (s *surface) windowID() ports.WindowID {
118	if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped {
119		return s.xdg.window.id
120	}
121	if s.layer != nil && s.layer.mapped {
122		return s.layer.id
123	}
124	return 0
125}
126
127// tree is the root's content with its subsurfaces flattened.
128func (s *surface) tree(id ports.WindowID) ports.SurfaceContent {
129	c := s.content
130	c.ID = id
131	c.Children = nil
132	for _, ch := range s.sub.children {
133		ch.appendTree(&c.Children, ch.sub.x, ch.sub.y, ch.sub.below)
134	}
135	if s.xdg != nil {
136		c.Geometry = s.xdg.geometry
137	}
138	c.Async = s.async
139	return c
140}
141
142// appendTree flattens a subsurface at (x, y) from the root: its children
143// below it, itself, then its children above.
144func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) {
145	for _, ch := range s.sub.children {
146		if ch.sub.below {
147			ch.appendTree(out, x+ch.sub.x, y+ch.sub.y, below)
148		}
149	}
150	if s.has {
151		*out = append(*out, ports.Subsurface{X: x, Y: y, Below: below, SurfaceContent: s.content})
152	}
153	for _, ch := range s.sub.children {
154		if !ch.sub.below {
155			ch.appendTree(out, x+ch.sub.x, y+ch.sub.y, below)
156		}
157	}
158}
159
160// redraw sends the tree of a mapped root to the outputs. Only a commit
161// of the root's own buffer with no other change keeps partial damage.
162func (s *surface) redraw() {
163	r := s.root()
164	if id := r.windowID(); id != 0 && s.server.channels.Contents != nil {
165		d := damage{full: true}
166		if s == r && len(r.sub.children) == 0 {
167			d = r.committed
168		}
169		s.server.emitContent(r.tree(id), d)
170	}
171}
172
173// detach removes a subsurface from its parent and redraws the parent.
174func (s *surface) detach() {
175	p := s.sub.parent
176	if p == nil {
177		return
178	}
179	for i, ch := range p.sub.children {
180		if ch == s {
181			p.sub.children = append(p.sub.children[:i:i], p.sub.children[i+1:]...)
182			break
183		}
184	}
185	s.sub.parent = nil
186	p.redraw()
187	s.sendScale()
188}
189
190func (s *surface) Destroy(*wayland.Surface) {
191	s.destroyed = true
192	for _, fb := range s.pendingFeedback {
193		discard(fb)
194	}
195	s.pendingFeedback = nil
196	s.tearing = nil // the control becomes inert
197	s.dropQueue()
198	if s.server.cursorSurface == s {
199		// The pointer keeps no cursor until the client sets another.
200		s.server.cursorSurface = nil
201		s.server.setCursor(ports.CursorChange{Hidden: true})
202	}
203	s.detach()
204	for _, ch := range s.sub.children {
205		ch.sub.parent = nil
206	}
207	s.sub.children = nil
208	for _, cb := range s.callbacks {
209		cb.Destroy()
210	}
211	s.callbacks = nil
212	// The client may reuse the buffer on another surface.
213	s.dropSync(s.pendingSync)
214	s.pendingSync = nil
215	if s.current != nil {
216		s.server.releaseBuffer(s, s.current, s.hold)
217		s.hold = syncHold{}
218	}
219	s.current, s.pending = nil, nil
220	if s.role != nil {
221		s.role(false)
222	}
223	if c := s.server.constraints[s]; c != nil {
224		s.server.dropConstraint(c)
225	}
226}
227func (s *surface) Attach(_ *wayland.Surface, b *wayland.Buffer, _, _ int32) {
228	s.pending = b
229	s.attached = true
230}
231func (s *surface) Frame(r *wayland.Surface, id uint32) {
232	cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{})
233	if err == nil {
234		s.callbacks = append(s.callbacks, cb)
235	}
236}
237func (s *surface) Commit(*wayland.Surface) {
238	if s.layer != nil && s.attached && s.pending != nil && !s.layer.acked {
239		s.layer.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "buffer before configure ack")
240		return
241	}
242	if s.xdg != nil && s.attached && s.pending != nil && !s.xdg.acked {
243		s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack")
244		return
245	}
246	if !s.checkSyncCommit() {
247		return
248	}
249	s.takeSyncPoints()
250	// A commit behind a fifo barrier, a future timestamp or an acquire
251	// point without a fence yet waits (fifo.go).
252	if s.mustWait(time.Now()) {
253		s.queueUpdate()
254		return
255	}
256	s.applyCommit()
257}
258
259// applyCommit makes the pending state current.
260func (s *surface) applyCommit() {
261	fresh := s.attached && s.pending != nil
262	if s.pendingBarrier {
263		s.pendingBarrier = false
264		s.setBarrier(time.Now())
265	}
266	s.pendingWait, s.pendingTime = false, time.Time{}
267	if s.contentKind != s.pendingKind {
268		s.contentKind = s.pendingKind
269		s.server.log.Info().Uint64("id", uint64(s.root().windowID())).Uint32("content_type", s.contentKind).Msg("content type")
270	}
271	if s.pendingScale > 0 {
272		s.bufferScale = s.pendingScale
273	}
274	hinted := s.async != s.pendingAsync
275	s.async = s.pendingAsync
276	if s.viewport != nil {
277		s.viewport.commit()
278	}
279	cs := s.pendingSync
280	s.pendingSync = nil
281	if s.attached {
282		if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {
283			s.server.releaseBuffer(s, s.current, s.hold)
284		} else if cs != nil || s.pending == nil {
285			// Same buffer again, or detached: the old points are done
286			// with once the new ones take over.
287			s.server.releaseBufferSync(s, s.hold)
288		}
289		s.hold = syncHold{}
290		s.current = s.pending
291		s.pending = nil
292		s.attached = false
293	}
294	s.applySync(cs)
295	if len(s.callbacks) > 0 {
296		s.server.queueFrames(s.server.frameOutput(s), s.callbacks)
297	}
298	s.callbacks = nil
299	if s.role != nil {
300		s.role(s.current != nil)
301	}
302	if fresh {
303		if state, ok := s.server.buffers[s.current.Resource]; ok {
304			if c, ok := state.content(0); ok {
305				c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height)
306				resized := !s.has || c.Width != s.content.Width || c.Height != s.content.Height || c.LogicalW != s.content.LogicalW || c.LogicalH != s.content.LogicalH
307				s.commitDamage(true, resized, c.Width, c.Height)
308				if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) {
309					s.lastW, s.lastH = c.Width, c.Height
310					s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size")
311				}
312				c.Acquire = s.hold.acquire
313				s.content, s.has = c, true
314			} else if shm, ok := state.(*buffer); ok {
315				shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")
316			}
317			// Buffers are read in place: released when the next one
318			// replaces them.
319		}
320	}
321	if !fresh {
322		s.commitDamage(false, false, 0, 0)
323	}
324	if s.current == nil {
325		s.content, s.has = ports.SurfaceContent{}, false
326	}
327	// Subsurface positions apply on the parent commit.
328	moved := false
329	for _, ch := range s.sub.children {
330		if ch.sub.moved {
331			ch.sub.x, ch.sub.y, ch.sub.moved = ch.sub.pendX, ch.sub.pendY, false
332			moved = true
333		}
334	}
335	geometry := false
336	if s.xdg != nil && s.xdg.pendingGeometry != s.xdg.geometry {
337		s.xdg.geometry, geometry = s.xdg.pendingGeometry, true
338	}
339	s.commitConstraint(geometry)
340	if s.xdg != nil && s.xdg.window != nil {
341		s.xdg.window.afterCommit()
342	}
343	drawn := fresh || moved || geometry || hinted || s.sub.parent != nil
344	if drawn {
345		if moved || geometry {
346			s.committed = damage{full: true}
347		}
348		s.redraw()
349	}
350	fb := s.pendingFeedback
351	s.pendingFeedback = nil
352	s.commitFeedback(fb, fresh)
353}
354func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) {
355	if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects {
356		s.pendingDamage = append(s.pendingDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)})
357	}
358}
359func (s *surface) DamageBuffer(_ *wayland.Surface, x, y, w, h int32) {
360	if w > 0 && h > 0 && len(s.pendingBufDamage) <= maxDamageRects {
361		s.pendingBufDamage = append(s.pendingBufDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)})
362	}
363}
364
365// commitDamage turns the requested damage into buffer pixels of a bw×bh
366// buffer. Surface damage scales by the buffer/logical ratio, rounded out.
367// A new size, too many rects or none at all with a new buffer (clients
368// that do not report damage) is a full change.
369func (s *surface) commitDamage(fresh, resized bool, bw, bh int) {
370	surf, buf := s.pendingDamage, s.pendingBufDamage
371	s.pendingDamage, s.pendingBufDamage = nil, nil
372	if !fresh {
373		s.committed = damage{}
374		return
375	}
376	if resized || len(surf)+len(buf) == 0 || len(surf)+len(buf) > maxDamageRects {
377		s.committed = damage{full: true}
378		return
379	}
380	lw, lh := s.logicalSize(bw, bh)
381	d := damage{rects: make([]ports.Rect, 0, len(surf)+len(buf))}
382	bounds := image.Rect(0, 0, bw, bh)
383	add := func(r image.Rectangle) {
384		if r = r.Intersect(bounds); !r.Empty() {
385			d.rects = append(d.rects, ports.Rect{X: r.Min.X, Y: r.Min.Y, W: r.Dx(), H: r.Dy()})
386		}
387	}
388	for _, r := range buf {
389		add(image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H))
390	}
391	for _, r := range surf {
392		if lw <= 0 || lh <= 0 {
393			s.committed = damage{full: true}
394			return
395		}
396		// Clients may damage INT32_MAX-sized rects: clamp before scaling.
397		x0, y0 := max(r.X, 0), max(r.Y, 0)
398		x1, y1 := min(r.X+min(r.W, lw), lw), min(r.Y+min(r.H, lh), lh)
399		add(image.Rect(x0*bw/lw, y0*bh/lh, (x1*bw+lw-1)/lw, (y1*bh+lh-1)/lh))
400	}
401	s.committed = d
402}
403func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {}
404func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region)  {}
405func (*surface) SetBufferTransform(*wayland.Surface, int32)        {}
406func (s *surface) SetBufferScale(r *wayland.Surface, v int32) {
407	if v < 1 {
408		r.PostError(uint32(wayland.SurfaceErrorInvalidScale), "buffer scale must be positive")
409		return
410	}
411	s.pendingScale = int(v)
412}
413func (*surface) Offset(*wayland.Surface, int32, int32) {}
414func (*surface) GetRelease(*wayland.Surface, uint32)   {}

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