v0.3.0 6800c6a

indicator_paint.go RawHistoryFolder

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

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