v0.2.0 6e0cddc

main.go RawHistoryFolder

1//go:build linux
2
3// Rect exercises presentation directly, without the immediate API.
4package main
5
6import (
7	"context"
8	"errors"
9	"flag"
10	"fmt"
11	"image"
12	"image/color"
13	"math"
14	"os"
15	"os/signal"
16	"path/filepath"
17	"runtime"
18	"strconv"
19	"strings"
20	"time"
21
22	"github.com/bnema/nefergui/internal/css"
23	"github.com/bnema/nefergui/internal/layout"
24	"github.com/bnema/nefergui/internal/presentation/session"
25	"github.com/bnema/nefergui/internal/presentation/vkdevice"
26	"github.com/bnema/nefergui/internal/render"
27	"github.com/bnema/nefergui/internal/text"
28)
29
30func main() {
31	var frames, captureFrame int
32	var size, textString string
33	var textScale float64
34	var transparent, static, list, styles, imageMode, changingImage bool
35	flag.BoolVar(&static, "static", false, "fixed rectangle and color for visual acceptance tests")
36	flag.BoolVar(&list, "list", false, "draw three overlapping instanced quads")
37	flag.BoolVar(&styles, "styles", false, "draw CSS-style rounded border, outline and shadow")
38	flag.BoolVar(&imageMode, "image", false, "draw two scaled checkerboard images")
39	flag.BoolVar(&changingImage, "changing-image", false, "image mode: pass a new image value each frame")
40	flag.StringVar(&textString, "text", "", "draw glyphs using the GPU atlas")
41	flag.Float64Var(&textScale, "scale", 1, "text mode: physical pixels per logical pixel")
42	flag.IntVar(&frames, "frames", 0, "frames before exit (0 = until close)")
43	flag.IntVar(&captureFrame, "capture-frame", 0, "static: committed frame count for capture request (0 = frames-8)")
44	flag.StringVar(&size, "size", "400x300", "initial logical window width x height")
45	flag.BoolVar(&transparent, "transparent", false, "use ARGB and premultiplied transparency")
46	flag.Parse()
47	width, height, err := parseSize(size)
48	if err == nil && static && frames > 0 {
49		if captureFrame == 0 {
50			captureFrame = frames - 8
51		}
52		if captureFrame < 1 || captureFrame >= frames {
53			err = fmt.Errorf("--capture-frame must be between 1 and --frames-1")
54		}
55	}
56	if err == nil && (textString != "" || imageMode || styles) && (textScale <= 0 || math.IsNaN(textScale) || math.IsInf(textScale, 0)) {
57		err = fmt.Errorf("invalid text scale")
58	}
59	if err != nil {
60		fmt.Fprintln(os.Stderr, err)
61		os.Exit(2)
62	}
63	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
64	defer stop()
65	ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
66	defer cancel()
67	s, err := session.Open("", width, height, transparent)
68	if err != nil {
69		fmt.Fprintln(os.Stderr, "presentation:", err)
70		os.Exit(1)
71	}
72	closed := false
73	defer func() {
74		if !closed {
75			_ = s.Close()
76		}
77	}()
78	fmt.Printf("presentation: ready main_device=%d render=%d:%d logical=%dx%d physical=%dx%d transparent=%v\n", s.Window.MainDevice, s.Device.Render.Major, s.Device.Render.Minor, s.Window.Width, s.Window.Height, s.Width, s.Height, transparent)
79	var requested time.Time
80	draw := func(i int) vkdevice.Rect {
81		if static {
82			// The render pass clears to transparent black (#00000000). XRGB
83			// displays that as opaque black. Straight #ff8000 at alpha 1
84			// reads back as #ff8000ff; at alpha 0.5 the shader writes
85			// premultiplied RGB (~128,64,0) and alpha ~128.
86			alpha := float32(1)
87			if transparent {
88				alpha = 0.5
89			}
90			return vkdevice.Rect{Bounds: [4]float32{0.25, 0.25, 0.5, 0.5}, Color: [4]float32{1, 128.0 / 255, 0, alpha}}
91		}
92		phase := float64(i) * 0.08
93		alpha := float32(1)
94		if transparent {
95			alpha = 0.5
96		}
97		return vkdevice.Rect{Bounds: [4]float32{float32(0.25 + 0.2*math.Sin(phase)), 0.25, 0.5, 0.5}, Color: [4]float32{float32(0.5 + 0.5*math.Sin(phase)), 0.25, 0.8, alpha}}
98	}
99	committed := func(committed int) error {
100		if static && committed == captureFrame && os.Getenv("NEFERGUI_DEBUG_DIR") != "" {
101			// Publish only a complete timestamp: the harness may observe this
102			// file while the client is still presenting subsequent frames.
103			dir := os.Getenv("NEFERGUI_DEBUG_DIR")
104			path := filepath.Join(dir, "capture-request")
105			tmp := path + ".tmp"
106			if err := os.WriteFile(tmp, []byte(strconv.FormatInt(time.Now().UnixNano(), 10)+"\n"), 0600); err != nil {
107				return err
108			}
109			if err := os.Rename(tmp, path); err != nil {
110				return err
111			}
112			requested = time.Now()
113		}
114		// Stay mapped long enough for the harness to see two later screenshots.
115		if static && committed == frames && !requested.IsZero() {
116			time.Sleep(time.Until(requested.Add(250 * time.Millisecond)))
117		}
118		return nil
119	}
120	var count int
121	if imageMode {
122		count, err = s.RunPreparedListWithCommit(ctx, frames, func(i int) (render.Frame, error) {
123			return s.Preparer.Prepare(imageCommands(i, changingImage, float64(s.Width)/textScale, float64(s.Height)/textScale), textScale, int(s.Width), int(s.Height))
124		}, committed)
125	} else if textString != "" {
126		_, source, _, _ := runtime.Caller(0)
127		catalog, e := text.Load(text.DirectorySource(filepath.Join(filepath.Dir(source), "..", "..", "testdata", "fonts")))
128		if e != nil {
129			fmt.Fprintln(os.Stderr, "text fonts:", e)
130			os.Exit(1)
131		}
132		measured, e := text.NewEngine(catalog).Measure(textString, text.Request{Families: []string{"Noto Sans"}, Size: 32}, 0)
133		if e != nil {
134			fmt.Fprintln(os.Stderr, "text measure:", e)
135			os.Exit(1)
136		}
137		cmd := layout.Command{Op: "text", Color: css.Color{R: 1, G: 1, B: 1, A: 1}, Opacity: 1}
138		for _, line := range measured.Lines {
139			for _, shaped := range line.Runs {
140				r := layout.Run{Face: shaped.Face, FaceID: shaped.Face.ID, Size: 32}
141				for _, g := range shaped.Glyphs {
142					r.Glyphs = append(r.Glyphs, layout.Glyph{ID: uint32(g.ID), X: 20 + g.X, Y: 40 + g.Y})
143				}
144				cmd.Runs = append(cmd.Runs, r)
145			}
146		}
147		count, err = s.RunPreparedListWithCommit(ctx, frames, func(int) (render.Frame, error) {
148			return s.Preparer.Prepare([]layout.Command{{Op: "rect", Rect: layout.Rect{W: float64(s.Width) / textScale, H: float64(s.Height) / textScale}, Color: css.Color{R: 17.0 / 255, G: 17.0 / 255, B: 17.0 / 255, A: 1}, Opacity: 1}, cmd}, textScale, int(s.Width), int(s.Height))
149		}, committed)
150	} else if styles {
151		count, err = s.RunPreparedListWithCommit(ctx, frames, func(int) (render.Frame, error) {
152			return s.Preparer.Prepare(styleCommands(), textScale, int(s.Width), int(s.Height))
153		}, committed)
154	} else if list {
155		count, err = s.RunListWithCommit(ctx, frames, func(int) []vkdevice.Instance { return listQuads(s.Width, s.Height) }, committed)
156	} else {
157		count, err = s.RunWithCommit(ctx, frames, draw, committed)
158	}
159	windowClosed := s.Window.Closed
160	closed = true
161	if cerr := s.Close(); cerr != nil && (err == nil || errors.Is(err, context.Canceled)) {
162		err = cerr
163	}
164	fmt.Printf("presentation: frames=%d closed=%v err=%v\n", count, windowClosed, err)
165	if err != nil && !errors.Is(err, context.Canceled) {
166		os.Exit(1)
167	}
168}
169
170// imageCommands mixes ordinary and image quads to exercise painter's order.
171func imageCommands(frame int, changing bool, width, height float64) []layout.Command {
172	checker := image.NewNRGBA(image.Rect(0, 0, 2, 2))
173	checker.SetNRGBA(0, 0, color.NRGBA{R: 255, A: 255})
174	checker.SetNRGBA(1, 0, color.NRGBA{G: 255, A: 255})
175	checker.SetNRGBA(0, 1, color.NRGBA{B: 255, A: 255})
176	checker.SetNRGBA(1, 1, color.NRGBA{R: 255, G: 255, A: 255})
177	if changing {
178		checker.Pix[0] = uint8(frame)
179	}
180	second := image.NewNRGBA(image.Rect(0, 0, 2, 2))
181	for y := 0; y < 2; y++ {
182		for x := 0; x < 2; x++ {
183			second.SetNRGBA(x, y, color.NRGBA{R: 255, G: 128, A: 255})
184		}
185	}
186	return []layout.Command{
187		{Op: "rect", Rect: layout.Rect{W: width, H: height}, Color: css.Color{R: 17.0 / 255, G: 17.0 / 255, B: 17.0 / 255, A: 1}, Opacity: 1},
188		{Op: "image", Rect: layout.Rect{X: 100, Y: 100, W: 64, H: 64}, Radii: [4]float64{8, 8, 8, 8}, Image: checker, Opacity: .5},
189		{Op: "rect", Rect: layout.Rect{X: 180, Y: 100, W: 20, H: 20}, Color: css.Color{R: 1, A: 1}, Opacity: 1},
190		{Op: "image", Rect: layout.Rect{X: 210, Y: 100, W: 64, H: 64}, Image: second, Opacity: 1},
191	}
192}
193
194func parseSize(size string) (int32, int32, error) {
195	w, h, ok := strings.Cut(size, "x")
196	if !ok {
197		return 0, 0, fmt.Errorf("invalid --size %q", size)
198	}
199	x, err := strconv.ParseInt(w, 10, 32)
200	if err != nil || x <= 0 {
201		return 0, 0, fmt.Errorf("invalid --size %q", size)
202	}
203	y, err := strconv.ParseInt(h, 10, 32)
204	if err != nil || y <= 0 {
205		return 0, 0, fmt.Errorf("invalid --size %q", size)
206	}
207	return int32(x), int32(y), nil
208}
209
210// listQuads uses physical pixels and keeps each probe away from antialiased edges.
211func listQuads(w, h int32) []vkdevice.Instance {
212	clip := [4]float32{0, 0, float32(w), float32(h)}
213	return []vkdevice.Instance{
214		{Bounds: [4]float32{100, 100, 200, 200}, Clip: clip, Radii: [4]float32{20, 20, 20, 20}, Color: [4]float32{1, 0, 0, 1}},
215		{Bounds: [4]float32{220, 120, 200, 200}, Clip: [4]float32{220, 120, 100, 200}, Color: [4]float32{0, 1, 0, 1}},
216		{Bounds: [4]float32{140, 180, 100, 100}, Clip: clip, Color: [4]float32{0, 0, 1, .5}},
217	}
218}
219
220// styleCommands uses logical coordinates; --scale converts them to physical.
221// Outset shadows precede the fill, while outlines follow the border.
222func styleCommands() []layout.Command {
223	box := layout.Rect{X: 120, Y: 120, W: 160, H: 120}
224	radius := [4]float64{20, 20, 20, 20}
225	sh := css.Shadow{X: css.Length{Value: 0, Unit: "px"}, Y: css.Length{Value: 8, Unit: "px"}, Blur: css.Length{Value: 12, Unit: "px"}, Color: css.Color{A: .9}}
226	return []layout.Command{
227		{Op: "rect", Rect: layout.Rect{W: 800, H: 600}, Color: css.Color{R: 17.0 / 255, G: 17.0 / 255, B: 17.0 / 255, A: 1}, Opacity: 1},
228		{Op: "shadow", Rect: box, Radii: radius, Shadow: &sh, Opacity: 1},
229		{Op: "rect", Rect: box, Radii: radius, Color: css.Color{R: 1, G: 1, B: 1, A: 1}, Opacity: 1},
230		{Op: "border", Rect: box, Radii: radius, Widths: layout.Edges{Top: 8, Right: 8, Bottom: 8, Left: 8}, Colors: css.ColorSides{Top: css.Color{R: 1, A: 1}, Right: css.Color{G: 1, A: 1}, Bottom: css.Color{B: 1, A: 1}, Left: css.Color{R: 1, G: 1, A: 1}}, Color: css.Color{A: 1}, Opacity: 1},
231		// Zero-width sides retain deliberately contrasting colors to catch
232		// accidental corner contributions from unpainted edges.
233		{Op: "border", Rect: layout.Rect{X: 300, Y: 40, W: 80, H: 50}, Widths: layout.Edges{Top: 8}, Colors: css.ColorSides{Top: css.Color{R: 1, A: 1}, Left: css.Color{G: 1, A: 1}, Right: css.Color{G: 1, A: 1}}, Color: css.Color{A: 1}, Opacity: 1},
234		{Op: "border", Rect: layout.Rect{X: 300, Y: 240, W: 80, H: 50}, Widths: layout.Edges{Top: 8, Left: 2}, Colors: css.ColorSides{Top: css.Color{R: 1, A: 1}, Left: css.Color{B: 1, A: 1}}, Color: css.Color{A: 1}, Opacity: 1},
235		// Layout has already expanded this rect by the 4px outline offset.
236		{Op: "outline", Rect: layout.Rect{X: 116, Y: 116, W: 168, H: 128}, Radii: radius, Widths: layout.Edges{Top: 3, Right: 3, Bottom: 3, Left: 3}, Color: css.Color{R: 1, G: 0, B: 1, A: 1}, Opacity: 1},
237	}
238}

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