v0.6.1 aa12db3

indicator_paint.go RawHistoryFolder

1package ui
2
3import (
4	"math"
5	"slices"
6
7	"github.com/bnema/nefergui/internal/css"
8	"github.com/bnema/nefergui/internal/layout"
9)
10
11// paintIndicators adds checkbox and radio indicators and slider tracks after
12// layout. They sit in the room the UA sheet reserves (checkbox and radio
13// padding-left, slider padding-bottom), are sized from font-size and colored
14// from accent-color and color, so author CSS drives them like any other paint.
15func (r *runtime) paintIndicators(root *element, out *layout.Output) {
16	var visit func(*element)
17	visit = func(e *element) {
18		if e == nil {
19			return
20		}
21		if (e.typ == "checkbox" || e.typ == "radio" || e.typ == "slider") && e.computed != nil {
22			id := idKey(e.identity)
23			if n := resultByID(out.Tree, id); n != nil {
24				insertOwnCommands(out, id, indicatorCommands(e, n, e.computed.Style))
25			}
26		}
27		for _, child := range e.children {
28			visit(child)
29		}
30	}
31	visit(root)
32}
33
34// indicatorCommands returns the paint for one control. The indicator is one
35// em square (16px when font-size is unset).
36func indicatorCommands(e *element, n *layout.Result, s *css.Style) []layout.Command {
37	size := s.FontSize.Value
38	if size <= 0 {
39		size = 16
40	}
41	base := layout.Command{ID: n.ID, Opacity: math.Min(1, math.Max(0, s.Opacity))}
42	muted := s.Color
43	muted.A *= 0.45
44	if e.typ == "slider" {
45		return sliderCommands(e, n, base, size, s.AccentColor, muted)
46	}
47	// Center the box in the reserved left padding, and vertically on the
48	// content box so it lines up with the label.
49	left := n.Border.X + n.BorderWidths.Left
50	x := left + math.Max(0, (n.Padding.Left-size)/2)
51	y := n.Content.Y + (n.Content.H-size)/2
52	box := layout.Rect{X: x, Y: y, W: size, H: size}
53	radius := size * 0.2
54	if e.typ == "radio" {
55		radius = size / 2
56	}
57	radii := [4]float64{radius, radius, radius, radius}
58	if !e.checked {
59		w := math.Max(1, math.Round(size/10))
60		cmd := base
61		// The renderer multiplies side alpha by Color alpha: keep Color opaque
62		// so the sides carry the muted alpha once.
63		opaque := muted
64		opaque.A = 1
65		cmd.Op, cmd.Rect, cmd.Radii, cmd.Color = "border", box, radii, opaque
66		cmd.Widths = layout.Edges{Top: w, Right: w, Bottom: w, Left: w}
67		cmd.Colors = css.ColorSides{Top: muted, Right: muted, Bottom: muted, Left: muted}
68		return []layout.Command{cmd}
69	}
70	fill := base
71	fill.Op, fill.Rect, fill.Radii, fill.Color = "rect", box, radii, s.AccentColor
72	// A contrasting inner mark: a dot for radios, a smaller square for
73	// checkboxes (the renderer paints rounded boxes, not glyph paths).
74	inner := size * 0.4
75	if e.typ == "checkbox" {
76		inner = size * 0.5
77	}
78	mark := base
79	markRadius := inner / 2
80	if e.typ == "checkbox" {
81		markRadius = inner * 0.2
82	}
83	mark.Op, mark.Color = "rect", css.Color{R: 1, G: 1, B: 1, A: 1}
84	mark.Rect = layout.Rect{X: x + (size-inner)/2, Y: y + (size-inner)/2, W: inner, H: inner}
85	mark.Radii = [4]float64{markRadius, markRadius, markRadius, markRadius}
86	return []layout.Command{fill, mark}
87}
88
89// sliderCommands paints the track under the label, in the reserved bottom
90// padding, across the content width the pointer maps to. The filled part and
91// the thumb follow the value fraction.
92func sliderCommands(e *element, n *layout.Result, base layout.Command, size float64, accent, muted css.Color) []layout.Command {
93	// Without reserved bottom padding there is no room: paint nothing rather
94	// than outside the control's box.
95	band := n.Padding.Bottom
96	if band <= 0 {
97		return nil
98	}
99	centerY := n.Content.Y + n.Content.H + band/2
100	trackH := math.Max(2, math.Round(size/4))
101	track := layout.Rect{X: n.Content.X, Y: centerY - trackH/2, W: n.Content.W, H: trackH}
102	radii := [4]float64{trackH / 2, trackH / 2, trackH / 2, trackH / 2}
103	rail := base
104	rail.Op, rail.Rect, rail.Radii, rail.Color = "rect", track, radii, muted
105	commands := []layout.Command{rail}
106	if e.fraction < 0 {
107		return commands
108	}
109	x := track.X + e.fraction*track.W
110	if filled := x - track.X; filled > 0 {
111		fill := base
112		fill.Op, fill.Radii, fill.Color = "rect", radii, accent
113		fill.Rect = layout.Rect{X: track.X, Y: track.Y, W: filled, H: track.H}
114		commands = append(commands, fill)
115	}
116	d := math.Min(size, band)
117	thumb := base
118	thumb.Op, thumb.Color = "rect", accent
119	thumb.Rect = layout.Rect{X: x - d/2, Y: centerY - d/2, W: d, H: d}
120	thumb.Radii = [4]float64{d / 2, d / 2, d / 2, d / 2}
121	return append(commands, thumb)
122}
123
124// insertOwnCommands places padding paint after the element's box paint and
125// before its content: at its clip-push when it clips (the clip is the content
126// box, which would hide the padding), otherwise after its last command.
127func insertOwnCommands(out *layout.Output, id string, commands []layout.Command) {
128	at := -1
129	for i := range out.Display {
130		if out.Display[i].ID != id {
131			continue
132		}
133		if out.Display[i].Op == "clip-push" {
134			at = i
135			break
136		}
137		at = i + 1
138	}
139	insertAt(out, at, commands)
140}
141
142// insertClippedCommands places content decorations inside the element's clip,
143// after its text and before its clip-pop. Editors always clip.
144func insertClippedCommands(out *layout.Output, id string, commands []layout.Command) {
145	for i := range out.Display {
146		if out.Display[i].ID == id && out.Display[i].Op == "clip-pop" {
147			insertAt(out, i, commands)
148			return
149		}
150	}
151}
152func insertAt(out *layout.Output, at int, commands []layout.Command) {
153	if at < 0 || len(commands) == 0 {
154		return
155	}
156	out.Display = slices.Insert(out.Display, at, commands...)
157}

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 1

Tags 12

↑↓ moveenter openesc close