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

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