main 5d6fa26

hintlabel.go RawHistoryFolder

1// Package hintlabel rasterizes the keyboard hint labels on the CPU: a small
2// box with the label text in Go Mono, as premultiplied BGRA pixels for the
3// renderer to upload and draw.
4package hintlabel
5
6import (
7	"image"
8	"image/color"
9	"image/draw"
10	"math"
11	"sync"
12
13	"golang.org/x/image/font"
14	"golang.org/x/image/font/gofont/gomono"
15	"golang.org/x/image/font/opentype"
16	"golang.org/x/image/math/fixed"
17)
18
19const (
20	// fontSize is the text size in logical pixels.
21	fontSize = 13
22	// padX and padY are the padding around the text in logical pixels.
23	padX, padY = 3, 1
24	// maxEntries bounds the cache: it is cleared when it grows past it.
25	maxEntries = 1024
26)
27
28var (
29	parseOnce sync.Once
30	parsed    *opentype.Font
31	parseErr  error
32)
33
34type key struct {
35	text  string
36	typed int
37	scale float64
38	style Style
39}
40
41type entry struct {
42	pix  []byte
43	w, h int
44}
45
46// Raster draws and caches labels. It is not safe for concurrent use: the
47// renderer owns it.
48type Raster struct {
49	faces map[float64]font.Face
50	cache map[key]entry
51}
52
53// Label returns the label box for text, whose first typed characters are
54// drawn dimmed, as premultiplied BGRA at scale physical pixels per logical
55// pixel. The slice is owned by the cache and stays valid until the cache is
56// cleared by a later call: do not modify it. Empty text, or a font that does
57// not load, yields a nil slice.
58func (r *Raster) Label(text string, typed int, scale float64, styles ...Style) (pix []byte, w, h int) {
59	if text == "" {
60		return nil, 0, 0
61	}
62	if scale <= 0 || math.IsNaN(scale) || math.IsInf(scale, 0) {
63		scale = 1
64	}
65	typed = max(0, min(typed, len(text)))
66	bg, fg := Colors("", "", "")
67	style := Style{fontSize * scale, bg, fg}
68	if len(styles) > 0 {
69		style = styles[0]
70	}
71	k := key{text, typed, scale, style}
72	if e, ok := r.cache[k]; ok {
73		return e.pix, e.w, e.h
74	}
75	face := r.face(style.FontPx)
76	if face == nil {
77		return nil, 0, 0
78	}
79	if r.cache == nil || len(r.cache) >= maxEntries {
80		r.cache = map[key]entry{}
81	}
82	e := render(face, text, typed, style)
83	r.cache[k] = e
84	return e.pix, e.w, e.h
85}
86
87func (r *Raster) face(scale float64) font.Face {
88	if f, ok := r.faces[scale]; ok {
89		return f
90	}
91	parseOnce.Do(func() { parsed, parseErr = opentype.Parse(gomono.TTF) })
92	if parseErr != nil {
93		return nil
94	}
95	f, err := opentype.NewFace(parsed, &opentype.FaceOptions{Size: scale, DPI: 72, Hinting: font.HintingFull})
96	if err != nil {
97		return nil
98	}
99	if r.faces == nil || len(r.faces) >= 16 {
100		for _, old := range r.faces {
101			_ = old.Close()
102		}
103		r.faces = map[float64]font.Face{}
104	}
105	r.faces[scale] = f
106	return f
107}
108
109// render draws an opaque box inside a one-physical-pixel translucent outline.
110// All colours are premultiplied.
111func render(face font.Face, text string, typed int, style Style) entry {
112	scale := style.FontPx / fontSize
113	background, ink := style.Background, style.Foreground
114	typedInk := mix(ink, background)
115	outline := color.RGBA{uint8(math.Round(float64(ink.R) * .35)), uint8(math.Round(float64(ink.G) * .35)), uint8(math.Round(float64(ink.B) * .35)), 89}
116	m := face.Metrics()
117	px := int(math.Round(padX * scale))
118	py := int(math.Round(padY * scale))
119	border := 1
120	adv := font.MeasureString(face, text).Ceil()
121	w := adv + 2*px + 2*border
122	h := m.Height.Ceil() + 2*py + 2*border
123	img := image.NewRGBA(image.Rect(0, 0, w, h))
124	draw.Draw(img, img.Bounds(), image.NewUniform(outline), image.Point{}, draw.Src)
125	draw.Draw(img, img.Bounds().Inset(border), image.NewUniform(background), image.Point{}, draw.Src)
126	d := font.Drawer{Dst: img, Face: face, Dot: fixed.Point26_6{X: fixed.I(border + px), Y: fixed.I(border+py) + m.Ascent}}
127	d.Src = image.NewUniform(typedInk)
128	d.DrawString(text[:typed])
129	d.Src = image.NewUniform(ink)
130	d.DrawString(text[typed:])
131	// Premultiplied RGBA to BGRA in place.
132	pix := img.Pix
133	for i := 0; i+3 < len(pix); i += 4 {
134		pix[i], pix[i+2] = pix[i+2], pix[i]
135	}
136	return entry{pix: pix, w: w, h: h}
137}

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