v0.3.0 da0887c

color.go RawHistoryFolder

1package tray
2
3import (
4	"math"
5
6	"github.com/bnema/neferbar/internal/theme"
7)
8
9// maxSamples caps the pixels counted per icon: enough for a color, and it
10// keeps the sums below 2^32 (4096 * 255 * 255) and the cost of a large icon
11// low.
12const maxSamples = 4096
13
14// histogram finds the dominant color of an icon. Colors are counted in 4096
15// buckets (4 bits per channel), weighted by opacity and saturation so a logo's
16// color wins over its grey outline. It is reused between icons: no
17// allocation.
18type histogram struct {
19	weight [4096]uint32
20	sum    [4096][3]uint32
21}
22
23// dominant returns the dominant color of argb, an SNI pixmap: big-endian
24// ARGB32, 4 bytes per pixel. It reports false when the icon is empty, fully
25// transparent or grey. Large icons are sampled evenly.
26func (h *histogram) dominant(argb []byte) (RGB, bool) {
27	if len(argb) < 4 {
28		return RGB{}, false
29	}
30	clear(h.weight[:])
31	clear(h.sum[:])
32	// An odd pixel step crosses every column of a power-of-two wide icon,
33	// so samples do not line up in a few columns.
34	step := (len(argb)/4 + maxSamples - 1) / maxSamples
35	if step > 1 {
36		step |= 1
37	}
38	for i := 0; i+3 < len(argb); i += 4 * step {
39		a, r, g, b := uint32(argb[i]), uint32(argb[i+1]), uint32(argb[i+2]), uint32(argb[i+3])
40		if a < 128 {
41			continue
42		}
43		hi, lo := max(r, g, b), min(r, g, b)
44		chroma := hi - lo
45		if chroma < 40 {
46			continue // grey, black or white: outline and shading
47		}
48		w := a * chroma >> 8
49		k := (r>>4)<<8 | (g>>4)<<4 | b>>4
50		h.weight[k] += w
51		h.sum[k][0] += r * w
52		h.sum[k][1] += g * w
53		h.sum[k][2] += b * w
54	}
55	best := -1
56	for k, w := range h.weight {
57		if w > 0 && (best < 0 || w > h.weight[best]) {
58			best = k
59		}
60	}
61	if best < 0 {
62		return RGB{}, false
63	}
64	w := h.weight[best]
65	s := h.sum[best]
66	return RGB{uint8(s[0] / w), uint8(s[1] / w), uint8(s[2] / w)}, true
67}
68
69// readable moves c toward fg until it has enough contrast with bg, so a dark
70// logo still shows on a dark bar.
71func readable(c, bg, fg RGB) RGB {
72	const minContrast = 3.0 // WCAG's minimum for large text and icons
73	for i := 0; i < 10 && contrast(c, bg) < minContrast; i++ {
74		c = theme.Mix(c, fg, 0.25)
75	}
76	return c
77}
78
79// contrast is the WCAG contrast ratio of two colors, from 1 to 21.
80func contrast(a, b RGB) float64 {
81	la, lb := luminance(a), luminance(b)
82	if la < lb {
83		la, lb = lb, la
84	}
85	return (la + 0.05) / (lb + 0.05)
86}
87
88func luminance(c RGB) float64 {
89	lin := func(v uint8) float64 {
90		x := float64(v) / 255
91		if x <= 0.04045 {
92			return x / 12.92
93		}
94		return math.Pow((x+0.055)/1.055, 2.4)
95	}
96	return 0.2126*lin(c[0]) + 0.7152*lin(c[1]) + 0.0722*lin(c[2])
97}

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 5

Tags 4

↑↓ moveenter openesc close