scene.go RawHistoryFolder

1package vulkan
2
3import (
4	"image"
5	"math"
6	"strconv"
7
8	"github.com/bnema/neferwl/internal/ports"
9)
10
11// sceneWalk turns one scene into quads in paint order, copying new wl_shm
12// content into GPU buffers on the way. It lives for one frame.
13type sceneWalk struct {
14	r        *Renderer
15	s        ports.Scene
16	contents map[ports.WindowID]ports.SurfaceContent
17	dmg      *damageRegion
18	// scale maps the scene's logical pixels to scene-physical ones.
19	scale float64
20	// zoom shrinks the surfaces of the window being placed: an overview
21	// preview (ports.SceneWindow.Preview); 1 otherwise.
22	zoom float64
23	// alpha is the opacity of the window being drawn, 1 - its Fade
24	// (ports.SceneWindow.Fade): its fills, lines and surfaces are scaled by
25	// it (premultiplied); 1 otherwise.
26	alpha float32
27	// rounded marks the window being drawn as rounded: it suppresses
28	// opaque-child culling (opaqueChildren), as the corners must show what
29	// is below.
30	rounded bool
31	bounds  image.Rectangle
32	// tileBounds is bounds cut to Scene.TileArea: where tiles and their
33	// lines draw.
34	tileBounds image.Rectangle
35	draws      []draw
36}
37
38// draws walks the scene into quads in paint order.
39func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, dmg *damageRegion) []draw {
40	clear(r.scratchDraws) // release references from longer earlier scenes
41	// The walk stays in scene-physical space (the target's size, swapped for
42	// a rotated output); orient maps the draws to the target afterwards.
43	sw, sh := r.sceneSize(s.Transform)
44	w := &sceneWalk{r: r, s: s, contents: contents, dmg: dmg, scale: s.Scale, zoom: 1, alpha: 1, bounds: image.Rect(0, 0, sw, sh), draws: r.scratchDraws[:0]}
45	r.scratchCovers = r.scratchCovers[:0]
46	if w.scale <= 0 {
47		w.scale = 1
48	}
49	w.layers(false)
50	outputBounds := w.bounds
51	if s.WorkspaceClip != (ports.Rect{}) {
52		c := s.WorkspaceClip
53		w.bounds = w.bounds.Intersect(w.physRect(c.X, c.Y, c.W, c.H))
54	}
55	w.tileBounds = w.bounds
56	if a, ok := s.TileArea(); ok {
57		w.tileBounds = w.bounds.Intersect(w.physRect(a.X, a.Y, a.W, a.H))
58	}
59	w.windows()
60	w.popups(false)
61	w.dropHints()
62	w.bounds = outputBounds
63	w.layers(true)
64	w.popups(true)
65	w.focusFlash()
66	w.captureIndicators()
67	w.positionHint()
68	w.hintLabels()
69	r.scratchDraws = w.draws
70	return w.draws
71}
72
73// focusFlash draws an inner border in logical coordinates, above the scene
74// but below capture indicators. Restore alpha so later overlays stay opaque.
75func (w *sceneWalk) focusFlash() {
76	f := w.s.Flash
77	if f == nil || f.Alpha <= 0 || f.Rect.W <= 0 || f.Rect.H <= 0 {
78		return
79	}
80	alpha := w.alpha
81	w.alpha = float32(f.Alpha)
82	b := min(f.Width, f.Rect.W/2, f.Rect.H/2)
83	if f.Radius > 0 && b > 0 {
84		w.roundedInnerBorder(f.Rect, b, f.Radius, parseColor(f.Color))
85	} else {
86		w.innerBorder(f.Rect, b, parseColor(f.Color))
87	}
88	w.alpha = alpha
89}
90
91// roundedInnerBorder draws the flash as one rounded ring: the target rect with
92// radius, minus a hole inset by b with radius max(radius-b, 0).
93func (w *sceneWalk) roundedInnerBorder(t ports.Rect, b, radius int, col [3]uint8) {
94	outer := w.physRect(t.X, t.Y, t.W, t.H)
95	r := float32(math.Round(float64(radius) * w.scale))
96	dr := w.fillRounded(outer, col, r)
97	if dr == nil {
98		return
99	}
100	dr.pc.misc[1] |= flagRing
101	hole := w.physRect(t.X+b, t.Y+b, t.W-2*b, t.H-2*b)
102	bPhys := min(hole.Min.X-outer.Min.X, hole.Min.Y-outer.Min.Y, outer.Max.X-hole.Max.X, outer.Max.Y-hole.Max.Y)
103	dr.roundInner = roundRect(hole)
104	dr.radii[1] = math.Float32bits(max(r-float32(max(bPhys, 0)), 0))
105}
106
107// innerBorder draws four non-overlapping strips, or fills a thin target.
108func (w *sceneWalk) innerBorder(t ports.Rect, b int, col [3]uint8) {
109	if b <= 0 {
110		w.fill(w.physRect(t.X, t.Y, t.W, t.H), col)
111		return
112	}
113	w.fill(w.physRect(t.X, t.Y, t.W, b), col)
114	w.fill(w.physRect(t.X, t.Y+t.H-b, t.W, b), col)
115	w.fill(w.physRect(t.X, t.Y+b, b, t.H-2*b), col)
116	w.fill(w.physRect(t.X+t.W-b, t.Y+b, b, t.H-2*b), col)
117}
118
119// captureIndicators draws the compositor's capture indicator over everything
120// (ports.CaptureIndicator). The border is four fills on the inside edge of the
121// target, so it stays visible on a monitor edge; the pill is a small rounded
122// square. Sizes are logical like the rest of the scene and scale with it. A
123// scene without indicators draws nothing: captures are rendered from scenes
124// that never carry them.
125func (w *sceneWalk) captureIndicators() {
126	if len(w.s.CaptureIndicators) == 0 {
127		return
128	}
129	col := parseColor(ports.CaptureBorderColor)
130	for _, m := range w.s.CaptureIndicators {
131		t := m.Rect
132		if t.W <= 0 || t.H <= 0 {
133			continue
134		}
135		if m.Pill {
136			w.pill(w.physRect(t.X, t.Y, t.W, t.H), col)
137			continue
138		}
139		w.innerBorder(t, ports.CaptureBorderOf(t.W, t.H), col)
140	}
141}
142
143// dropHints fills where a dragged tile would land (ports.Scene.DropHints),
144// in the active border color, over the workspace's windows.
145func (w *sceneWalk) dropHints() {
146	if len(w.s.DropHints) == 0 {
147		return
148	}
149	col := parseColor(w.s.Border.Active)
150	for _, r := range w.s.DropHints {
151		if r.W > 0 && r.H > 0 {
152			w.fill(w.physRect(r.X, r.Y, r.W, r.H), col)
153		}
154	}
155}
156
157// pill fills a rounded square (physical pixels) with three rects: a full
158// middle and two bands narrowed by the corner radius.
159func (w *sceneWalk) pill(r image.Rectangle, c [3]uint8) {
160	rad := min(w.phys(ports.CapturePillRadius), r.Dx()/2, r.Dy()/2)
161	if rad <= 0 {
162		w.fill(r, c)
163		return
164	}
165	cut := (rad + 1) / 2
166	w.fill(image.Rect(r.Min.X+cut, r.Min.Y, r.Max.X-cut, r.Min.Y+rad), c)
167	w.fill(image.Rect(r.Min.X, r.Min.Y+rad, r.Max.X, r.Max.Y-rad), c)
168	w.fill(image.Rect(r.Min.X+cut, r.Max.Y-rad, r.Max.X-cut, r.Max.Y), c)
169}
170
171func (w *sceneWalk) phys(v int) int { return int(math.Round(float64(v) * w.scale)) }
172
173func (w *sceneWalk) physRect(x, y, width, height int) image.Rectangle {
174	return image.Rect(w.phys(x), w.phys(y), w.phys(x+width), w.phys(y+height))
175}
176
177// physRectF is physRect for fractional logical coordinates (zoomed surfaces).
178func (w *sceneWalk) physRectF(x, y, width, height float64) image.Rectangle {
179	p := func(v float64) int { return int(math.Round(v * w.scale)) }
180	return image.Rect(p(x), p(y), p(x+width), p(y+height))
181}
182
183// fill draws a solid rect (physical pixels), at the walk's alpha.
184func (w *sceneWalk) fill(rect image.Rectangle, c [3]uint8) {
185	rect = rect.Intersect(w.bounds)
186	if rect.Empty() {
187		return
188	}
189	dr := w.r.fillDraw(rect, c)
190	if w.alpha < 1 {
191		w.r.fadeSolid(&dr, w.alpha)
192	}
193	w.draws = append(w.draws, dr)
194}
195
196// fillRounded is fill with rounded corners of radius (physical pixels). It
197// returns the new draw, or nil when fill emitted nothing or radius <= 0 (the
198// plain fill is kept). The pointer is valid only until the next append.
199func (w *sceneWalk) fillRounded(r image.Rectangle, c [3]uint8, radius float32) *draw {
200	n := len(w.draws)
201	w.fill(r, c)
202	if len(w.draws) == n || radius <= 0 {
203		return nil
204	}
205	dr := &w.draws[n]
206	roundedDraw(dr, r, radius)
207	return dr
208}
209
210// dim paints a translucent black quad over rect (physical), clipped to the
211// output, at alpha scaled by the walk's alpha. Damage clipping later limits
212// it to the region being repainted, just like other fills.
213func (w *sceneWalk) dim(rect image.Rectangle, alpha float64) {
214	if rect = rect.Intersect(w.bounds); !rect.Empty() {
215		w.draws = append(w.draws, w.r.dimDraw(rect, alpha*float64(w.alpha)))
216	}
217}
218
219// layers draws the layer surfaces below the windows, or above them.
220// The scene holds only the shown ones: core hides them under a fullscreen
221// window.
222func (w *sceneWalk) layers(afterWindows bool) {
223	for _, layer := range w.s.Layers {
224		if (layer.Layer >= ports.LayerTop) != afterWindows || layer.Rect.W <= 0 || layer.Rect.H <= 0 {
225			continue
226		}
227		content := w.contents[layer.ID]
228		w.dmg.window(layer.ID, content, w.physRect(layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H))
229		w.place(layer.ID, &content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)
230	}
231}
232
233// drawsAsWindow reports whether windows() paints win as a window (not
234// hidden, a popup or empty).
235func drawsAsWindow(win ports.SceneWindow) bool {
236	return !win.Hidden && !win.Popup && win.Rect.W > 0 && win.Rect.H > 0
237}
238
239// opensFloats reports whether win, once reached, closes the tiles: the tile
240// lines and the dim veil are painted under it (an overview preview of a
241// float is not one).
242func opensFloats(win ports.SceneWindow) bool {
243	return !win.Below && win.Floating && win.Preview == 0
244}
245
246// floatsStart is the index of the first window that opens the floats in s,
247// or -1 when none does: where windows() splits its paint order.
248func floatsStart(s ports.Scene) int {
249	for i, win := range s.Windows {
250		if drawsAsWindow(win) && opensFloats(win) {
251			return i
252		}
253	}
254	return -1
255}
256
257// windows draws ordered placements with borders. Tile lines go after
258// the column group, before the first float above it; an overview preview
259// of a float is not one. The veil goes under that float, or under every
260// window with DimBehind.
261func (w *sceneWalk) windows() {
262	if w.s.DimBehind && w.s.Dim > 0 {
263		w.dim(w.bounds, w.s.Dim)
264	}
265	tileLines := false
266	for _, win := range w.s.Windows {
267		if !drawsAsWindow(win) {
268			continue
269		}
270		if opensFloats(win) && !tileLines {
271			w.tileSeparators()
272			tileLines = true
273			if w.s.Dim > 0 && !w.s.DimBehind {
274				w.dim(w.bounds, w.s.Dim)
275			}
276		}
277		if win.Fade >= 1 {
278			// Faded out: it still opens the floats (its lines and veil are
279			// painted under where it would be), but draws nothing.
280			continue
281		}
282		w.alpha = 1 - float32(max(0, win.Fade))
283		windowBounds := w.bounds
284		if win.Tile() {
285			// A tile never draws over a panel (Scene.TileArea).
286			w.bounds = w.tileBounds
287		}
288		// Content sits inside the border; core sized the client to match.
289		b, inset := 0, ports.Sides(0)
290		if !win.Fullscreen {
291			b, inset = w.s.Border.Width, win.Inset
292		}
293		c := win.Rect.Inset(inset, b)
294		body := w.physRect(c.X, c.Y, c.W, c.H)
295		content := w.contents[win.ID]
296		// Damage counts only what is drawn: never the panel under a tile.
297		outer := w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H)
298		w.dmg.window(win.ID, content, outer.Intersect(w.bounds))
299		w.rounded = win.Radius > 0
300		var radius, innerRadius float32
301		if win.Radius > 0 {
302			radius = float32(math.Round(float64(win.Radius) * w.scale))
303			// The border's physical width, from the rects actually drawn.
304			bPhys := min(body.Min.X-outer.Min.X, body.Min.Y-outer.Min.Y, outer.Max.X-body.Max.X, outer.Max.Y-body.Max.Y)
305			innerRadius = max(radius-float32(max(bPhys, 0)), 0)
306			if col := w.ringColor(win.ID); col != "" && b > 0 {
307				if dr := w.fillRounded(outer, parseColor(col), radius); dr != nil {
308					dr.pc.misc[1] |= flagRing
309					// Opaque, the ring runs 1px under the body so no seam
310					// shows; faded, that overlap would double up: the hole
311					// is the body.
312					hole, holeRadius := body.Inset(1), max(innerRadius-1, 0)
313					if w.alpha < 1 {
314						hole, holeRadius = body, innerRadius
315					}
316					dr.roundInner = roundRect(hole)
317					dr.radii[1] = math.Float32bits(holeRadius)
318				}
319			}
320		}
321		start := len(w.draws)
322		// Until its first buffer, a window shows the background: no flash.
323		w.fill(body, parseColor(w.s.Background))
324		if !content.Empty() {
325			// An animating frame zooms its content (Zoom); a card draws
326			// at its Preview.
327			if win.Zoom > 0 {
328				w.zoom = win.Zoom
329			} else if win.Preview > 0 {
330				w.zoom = win.Preview
331			}
332			w.place(win.ID, &content, c.X, c.Y, c.W, c.H)
333			w.zoom = 1
334		}
335		if win.Radius > 0 {
336			for j := start; j < len(w.draws); j++ {
337				roundedDraw(&w.draws[j], body, innerRadius)
338			}
339		} else {
340			w.separators(win.ID)
341		}
342		dimStart := len(w.draws)
343		if win.Dim > 0 {
344			// A stashed window peeking in: dimmed with its border.
345			w.dim(outer, win.Dim)
346		}
347		if win.Radius > 0 {
348			for j := dimStart; j < len(w.draws); j++ {
349				roundedDraw(&w.draws[j], outer, radius)
350			}
351		}
352		w.rounded = false
353		w.alpha, w.bounds = 1, windowBounds
354	}
355	if !tileLines {
356		w.tileSeparators()
357	}
358}
359
360// tileSeparators draws the tile lines, cut like the tiles at the panels.
361func (w *sceneWalk) tileSeparators() {
362	b := w.bounds
363	w.bounds = w.tileBounds
364	w.separators(0)
365	if o := w.s.Outline; o != nil {
366		w.roundedInnerBorder(o.Rect, o.Width, o.Radius, parseColor(w.s.Border.Active))
367	}
368	w.bounds = b
369}
370
371// ringColor is the border color of the window id's lines: the inactive one,
372// or the active one when any of its lines is active; "" without lines.
373func (w *sceneWalk) ringColor(id ports.WindowID) string {
374	col := ""
375	for _, sep := range w.s.Separators {
376		if sep.Window != id {
377			continue
378		}
379		if sep.Active {
380			return w.s.Border.Active
381		}
382		col = w.s.Border.Inactive
383	}
384	return col
385}
386
387// separators draws a window's border lines in order, active lines over
388// inactive ones.
389func (w *sceneWalk) separators(id ports.WindowID) {
390	for _, sep := range w.s.Separators {
391		if sep.Window != id {
392			continue
393		}
394		col := w.s.Border.Inactive
395		if sep.Active {
396			col = w.s.Border.Active
397		}
398		if col != "" {
399			w.fill(w.physRect(sep.Rect.X, sep.Rect.Y, sep.Rect.W, sep.Rect.H), parseColor(col))
400		}
401	}
402}
403
404// popups draws only what the client drew, shadows clipped. Window popups
405// stay with the windows; a layer's go over every layer.
406func (w *sceneWalk) popups(overLayers bool) {
407	for _, win := range w.s.Windows {
408		if win.Popup && win.OverLayers == overLayers && !win.Hidden && win.Rect.W > 0 && win.Rect.H > 0 {
409			content := w.contents[win.ID]
410			w.dmg.window(win.ID, content, w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H))
411			w.place(win.ID, &content, win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H)
412		}
413	}
414}
415
416// place draws a surface tree with its window geometry at (x, y) logical,
417// clipped to width×height logical: client shadows fall outside. Every
418// surface of the tree is keyed by its stable surface identity.
419func (w *sceneWalk) place(id ports.WindowID, content *ports.SurfaceContent, x, y, width, height int) {
420	clip := image.Rect(x, y, x+width, y+height)
421	// Offsets inside the tree shrink with a zoomed (preview) window.
422	z := w.zoom
423	ox, oy := float64(x)-float64(content.Geometry.X)*z, float64(y)-float64(content.Geometry.Y)*z
424	at := func(ch *ports.Subsurface) (float64, float64) { return ox + float64(ch.X)*z, oy + float64(ch.Y)*z }
425	covers := w.opaqueChildren(content, ox, oy, clip)
426	for i := range content.Children {
427		if ch := &content.Children[i]; ch.Below {
428			cx, cy := at(ch)
429			w.surface(&ch.SurfaceContent, cx, cy, clip, shmKey{win: id, surface: ch.Surface}, ch.Version, covers, false)
430		}
431	}
432	w.surface(content, ox, oy, clip, shmKey{win: id, surface: content.Surface}, content.Version, covers, true)
433	for i := range content.Children {
434		if ch := &content.Children[i]; !ch.Below {
435			cx, cy := at(ch)
436			w.surface(&ch.SurfaceContent, cx, cy, clip, shmKey{win: id, surface: ch.Surface}, ch.Version, nil, false)
437		}
438	}
439}
440
441// opaqueChildren gives the physical rects shown opaque by the subsurfaces
442// above the root (e.g. a game's GPU surface over its window's shm buffer):
443// they hide the surfaces below them. A child that is not wholly opaque but
444// has an opaque region (Firefox's page, whose region stops a pixel short of
445// its edges) hides the surfaces below only inside that rect. A child whose
446// buffer will not draw is left out, so the surface below it still shows: a
447// dmabuf that fails to import (opaqueChildren pre-imports it) or an
448// unreadable shm buffer.
449func (w *sceneWalk) opaqueChildren(content *ports.SurfaceContent, ox, oy float64, clip image.Rectangle) []image.Rectangle {
450	if w.rounded {
451		return nil
452	}
453	covers := w.r.scratchCovers[:0]
454	for i := range content.Children {
455		ch := &content.Children[i]
456		if ch.Below || !ch.Opaque && ch.OpaqueRect.W <= 0 {
457			continue
458		}
459		full, dst, ok := w.surfaceRects(&ch.SurfaceContent, ox+float64(ch.X)*w.zoom, oy+float64(ch.Y)*w.zoom, clip)
460		if !ok || !w.drawable(&ch.SurfaceContent) {
461			continue
462		}
463		if !ch.Opaque {
464			if dst = opaqueCover(&ch.SurfaceContent, full, dst); dst.Empty() {
465				continue
466			}
467		}
468		covers = append(covers, dst)
469	}
470	w.r.scratchCovers = covers
471	return covers
472}
473
474// opaqueCover is the part of dst (physical, what the surface shows of full)
475// that its OpaqueRect maps to. Edges round inward, so every pixel of it is
476// inside the region; an edge on the surface's own edge stays on dst's.
477func opaqueCover(c *ports.SurfaceContent, full, dst image.Rectangle) image.Rectangle {
478	lw, lh := c.LogicalW, c.LogicalH
479	if lw <= 0 || lh <= 0 {
480		lw, lh = c.Width, c.Height
481	}
482	o := c.OpaqueRect
483	if lw <= 0 || lh <= 0 || o.W <= 0 || o.H <= 0 {
484		return image.Rectangle{}
485	}
486	// ceil and floor of full.Min + v*size/n, v >= 0.
487	lo := func(min, v, size, n int) int { return min + (v*size+n-1)/n }
488	hi := func(min, v, size, n int) int { return min + v*size/n }
489	r := image.Rect(lo(full.Min.X, max(o.X, 0), full.Dx(), lw), lo(full.Min.Y, max(o.Y, 0), full.Dy(), lh),
490		hi(full.Min.X, min(o.X+o.W, lw), full.Dx(), lw), hi(full.Min.Y, min(o.Y+o.H, lh), full.Dy(), lh))
491	return r.Intersect(dst)
492}
493
494// drawable reports whether the child's buffer will draw. GPU buffer
495// imports are cached, so pre-importing here costs nothing.
496func (w *sceneWalk) drawable(c *ports.SurfaceContent) bool {
497	if c.Solid != nil {
498		return true
499	}
500	if c.DMABuf != nil {
501		_, err := w.r.importDMABuf(c.DMABuf)
502		return err == nil
503	}
504	b := c.SHM
505	if b == nil {
506		return false
507	}
508	_, err := w.r.shmPixels(b, b.Offset+(c.Height-1)*b.Stride+c.Width*4)
509	return err == nil
510}
511
512// surfaceRects gives the physical rect of a surface buffer with its origin
513// at (x, y) logical, shrunk by the walk's zoom, and the part of it clip
514// (logical) shows. ok is false when there is nothing to draw.
515func (w *sceneWalk) surfaceRects(content *ports.SurfaceContent, x, y float64, clip image.Rectangle) (full, dst image.Rectangle, ok bool) {
516	lw, lh := content.LogicalW, content.LogicalH
517	if lw <= 0 || lh <= 0 {
518		lw, lh = content.Width, content.Height
519	}
520	if !content.HasBuffer() || lw <= 0 || lh <= 0 || content.Width <= 0 || content.Height <= 0 {
521		return full, dst, false
522	}
523	if content.SHM != nil && (content.SHM.Stride < content.Width*4 || content.SHM.Offset < 0) {
524		return full, dst, false
525	}
526	full = w.physRectF(x, y, float64(lw)*w.zoom, float64(lh)*w.zoom)
527	// A buffer drawn at the physical size (fractional-scale clients round
528	// w*scale, we round per edge) is copied 1:1, never resampled for 1px.
529	near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }
530	sourceW, sourceH := content.Width, content.Height
531	if content.Source[2] > 0 {
532		sourceW, sourceH = int(content.Source[2]), int(content.Source[3])
533	}
534	sourceW, sourceH = content.Transform.Size(sourceW, sourceH)
535	if near(full.Dx(), sourceW) && near(full.Dy(), sourceH) {
536		full.Max = full.Min.Add(image.Pt(sourceW, sourceH))
537	}
538	dst = full.Intersect(w.physRect(clip.Min.X, clip.Min.Y, clip.Dx(), clip.Dy())).Intersect(w.bounds)
539	return full, dst, !dst.Empty()
540}
541
542// surface draws one surface buffer with its origin at (x, y) logical,
543// clipped to clip (logical), unless an opaque surface above it (covers,
544// physical) hides all of it: its buffer is then neither copied nor drawn.
545// A cover over only part of it clips the draw to the rest.
546func (w *sceneWalk) surface(content *ports.SurfaceContent, x, y float64, clip image.Rectangle, key shmKey, seq uint64, covers []image.Rectangle, root bool) {
547	full, dst, ok := w.surfaceRects(content, x, y, clip)
548	if !ok {
549		return
550	}
551	for _, c := range covers {
552		if dst.In(c) {
553			return
554		}
555	}
556	if root {
557		w.dmg.content(key.win, content, full, dst)
558	}
559	n := len(w.draws)
560	w.content(dst, full, content, key, seq, root)
561	if len(w.draws) == n+1 && len(covers) > 0 {
562		// The quad's mapping comes from full: only its rect shrinks.
563		d := w.draws[n]
564		w.draws = w.draws[:n]
565		w.split(d, dst, covers)
566	}
567}
568
569// Splitting a quad around covers is bounded: covers come from the client, and
570// each can cut a quad into four strips, recursively. Past these bounds the
571// quad is drawn whole: the covers above it hide the rest, at the cost of
572// overdraw.
573const (
574	maxSplitCovers = 8  // covers crossing the quad that are still split around
575	maxSplitStrips = 32 // draws one quad may become
576)
577
578// split appends d, a quad over rect, minus the covers, or the whole quad
579// when the covers cut it into too many strips.
580func (w *sceneWalk) split(d draw, rect image.Rectangle, covers []image.Rectangle) {
581	crossing := 0
582	for _, c := range covers {
583		if !rect.Intersect(c).Empty() {
584			crossing++
585		}
586	}
587	n := len(w.draws)
588	if crossing <= maxSplitCovers {
589		w.uncovered(d, rect, covers, n+maxSplitStrips)
590		if len(w.draws) <= n+maxSplitStrips {
591			return
592		}
593		w.draws = w.draws[:n]
594	}
595	w.quad(d, rect)
596}
597
598// uncovered appends d, a quad over rect, minus the covers: the strips of
599// rect around each cover, in turn. d keeps its mapping. It stops once the
600// draws pass limit, for the caller to drop them.
601func (w *sceneWalk) uncovered(d draw, rect image.Rectangle, covers []image.Rectangle, limit int) {
602	if len(w.draws) > limit {
603		return
604	}
605	for i, c := range covers {
606		in := rect.Intersect(c)
607		if in.Empty() {
608			continue
609		}
610		rest := covers[i+1:]
611		w.uncovered(d, image.Rect(rect.Min.X, rect.Min.Y, rect.Max.X, in.Min.Y), rest, limit)
612		w.uncovered(d, image.Rect(rect.Min.X, in.Max.Y, rect.Max.X, rect.Max.Y), rest, limit)
613		w.uncovered(d, image.Rect(rect.Min.X, in.Min.Y, in.Min.X, in.Max.Y), rest, limit)
614		w.uncovered(d, image.Rect(in.Max.X, in.Min.Y, rect.Max.X, in.Max.Y), rest, limit)
615		return
616	}
617	w.quad(d, rect)
618}
619
620// quad appends d over rect, unless it is empty.
621func (w *sceneWalk) quad(d draw, rect image.Rectangle) {
622	if rect.Empty() {
623		return
624	}
625	d.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)}
626	w.draws = append(w.draws, d)
627}
628
629// content draws the part of a buffer mapped onto full (physical pixels)
630// that dst shows. key names the surface, seq its content.
631func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceContent, key shmKey, seq uint64, root bool) {
632	r := w.r
633	rect := dst
634	if rect.Empty() {
635		return
636	}
637	if content.Solid != nil {
638		dr := r.solidDraw(rect, content)
639		if w.alpha < 1 {
640			r.fadeSolid(&dr, w.alpha)
641		}
642		if dr.pc.color[3] > 0 {
643			w.draws = append(w.draws, dr)
644		}
645		return
646	}
647	if content.DMABuf != nil {
648		im, err := r.importDMABuf(content.DMABuf)
649		if err != nil {
650			// The window shows its background until a buffer imports.
651			return
652		}
653		dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeImage, content.Opaque)
654		r.setContentColor(&dr, content)
655		dr.pc.color[3] *= w.alpha
656		if im.yuv {
657			dr.pc.misc[1] |= flagYUV
658			if content.DMABuf.Format == fourcc('P', '0', '1', '0') {
659				dr.pc.misc[1] |= flagP010
660			}
661			coeff, ran := content.Color.Coefficients, content.Color.Range
662			if coeff == 0 {
663				coeff = 2 /* BT.709 */
664			}
665			if ran == 0 {
666				ran = 2 /* limited */
667			}
668			dr.pc.buf[3] = uint32(coeff) | uint32(ran)<<8 | uint32(content.Color.Chroma)<<16
669		}
670		dr.im = im
671		dr.acquire = content.Acquire
672		w.draws = append(w.draws, dr)
673		return
674	}
675	b := content.SHM
676	st := shmState{w: content.Width, h: content.Height, seq: seq, windowSeq: content.Seq}
677	// The copy of this content, if any, needs no pool: a kept (closing)
678	// window whose pool file is closed still draws.
679	c := r.shmCached(key, st)
680	if c != nil {
681		// As a drawn pool, for Trim; nothing is mapped for it.
682		if m := r.pools[b.Pool]; m != nil {
683			m.last = r.frame
684		}
685	} else {
686		pixels, err := r.shmPixels(b, b.Offset+(content.Height-1)*b.Stride+content.Width*4)
687		if err != nil {
688			return
689		}
690		// Damage history is the root surface's: children copy in full.
691		var damage func(uint64) ([]ports.Rect, bool)
692		if root {
693			damage = content.DamageSince
694		}
695		if c, err = r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage); err != nil {
696			return
697		}
698	}
699	dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeBuffer, content.Opaque)
700	r.setContentColor(&dr, content)
701	dr.pc.color[3] *= w.alpha
702	dr.shm = c
703	dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0}
704	w.draws = append(w.draws, dr)
705}
706
707// setContentColor selects only color encodings the protocol accepts, and
708// the surface's fade.
709func (r *Renderer) setContentColor(dr *draw, content *ports.SurfaceContent) {
710	dr.pc.color[3] = 1 - max(0, min(1, content.Fade))
711	c := content.Color
712	if c.IsPQ2020() {
713		dr.pc.misc[1] |= flagPQ
714	} else if c.IsExtendedLinear() {
715		dr.pc.misc[1] |= flagExtendedLinear
716	} else {
717		return
718	}
719	dr.pc.color[0] = float32(r.hdrNits)
720	if dr.pc.color[0] <= 0 {
721		dr.pc.color[0] = 203
722	}
723}
724
725func parseColor(s string) [3]uint8 {
726	var c [3]uint8
727	if len(s) != 7 || s[0] != '#' {
728		return c
729	}
730	for i := range c {
731		v, e := strconv.ParseUint(s[1+i*2:3+i*2], 16, 8)
732		if e != nil {
733			return [3]uint8{}
734		}
735		c[i] = uint8(v)
736	}
737	return c
738}
739
740// roundRect is r as float bounds for the shader's rounded coverage. Rounded
741// bounds stay independent of scissor/damage clipping.
742func roundRect(r image.Rectangle) [4]float32 {
743	return [4]float32{float32(r.Min.X), float32(r.Min.Y), float32(r.Max.X), float32(r.Max.Y)}
744}
745
746// roundedDraw makes d a rounded draw: the shader clips it to rect with
747// corners of radius (physical pixels).
748func roundedDraw(d *draw, rect image.Rectangle, radius float32) {
749	d.pc.misc[1] |= flagRounded
750	d.round = roundRect(rect)
751	d.radii[0] = math.Float32bits(radius)
752}

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