v0.1.0 47aaa0f

prepare.go RawHistoryFolder

1// Package render prepares ordered physical-pixel primitives for Vulkan submission.
2// It does not own the display list or its borrowed font faces.
3package render
4
5import (
6	"fmt"
7	"image"
8	"math"
9
10	"github.com/bnema/nefergui/internal/css"
11	"github.com/bnema/nefergui/internal/layout"
12	"github.com/bnema/nefergui/internal/text"
13	"github.com/go-text/typesetting/font"
14)
15
16// Quad is a physical-pixel rectangle with a physical clip. Color is straight
17// sRGB; conversion to premultiplied linear color belongs at the GPU boundary.
18// Glyphs refer to an atlas placement valid only for this prepared frame.
19type Quad struct {
20	Op     string
21	Rect   layout.Rect
22	Clip   layout.Rect
23	Color  css.Color
24	Radii  [4]float64
25	Widths layout.Edges
26	Colors css.ColorSides
27	Glyph  text.Placement
28	Image  image.Image
29	// Shape is the original box when the draw bounds extend past it.
30	Shape layout.Rect
31	// Shadow holds physical offset, blur and spread. Inset is carried separately.
32	Shadow [4]float64
33	Inset  bool
34}
35
36type Frame struct {
37	Quads   []Quad
38	Uploads []text.Upload
39}
40
41// Preparer owns the glyph atlas and the obligation to deliver its changes to the
42// GPU mirror. Prepare hands the caller every change not yet known to be
43// submitted; Submitted is the only acknowledgement. A frame that is retried,
44// abandoned, superseded or never reached submission therefore never loses
45// atlas changes: the next Prepare resends whole pages (placements are kept).
46type Preparer struct {
47	Atlas *text.Atlas
48	// owed is true while the latest issued uploads have not been submitted.
49	owed   bool
50	issued []text.Upload
51}
52
53// NewPreparer bounds the grayscale glyph atlas. Call Prepare synchronously with
54// layout; the atlas and shaped faces are not safe for concurrent use.
55func NewPreparer(size, pages int) (*Preparer, error) {
56	a, err := text.NewAtlas(size, pages)
57	if err != nil {
58		return nil, err
59	}
60	return &Preparer{Atlas: a}, nil
61}
62
63func intersect(a, b layout.Rect) layout.Rect {
64	x, y := math.Max(a.X, b.X), math.Max(a.Y, b.Y)
65	return layout.Rect{X: x, Y: y, W: math.Max(0, math.Min(a.X+a.W, b.X+b.W)-x), H: math.Max(0, math.Min(a.Y+a.H, b.Y+b.H)-y)}
66}
67
68func physical(r layout.Rect, scale float64) layout.Rect {
69	x, y, w, h := layout.Physical(r, scale)
70	return layout.Rect{X: float64(x), Y: float64(y), W: float64(w), H: float64(h)}
71}
72
73func scaled(e layout.Edges, scale float64) layout.Edges {
74	return layout.Edges{Top: e.Top * scale, Right: e.Right * scale, Bottom: e.Bottom * scale, Left: e.Left * scale}
75}
76
77// maxQuadPrealloc bounds speculative storage for mostly clipped documents.
78// Visible frames larger than this grow normally as their quads are emitted.
79const maxQuadPrealloc = 512
80
81// quadCapacity estimates one quad per glyph and drawable command, capped so
82// offscreen content cannot force a large allocation before culling.
83func quadCapacity(commands []layout.Command) int {
84	n := 0
85	for i := range commands {
86		switch cmd := &commands[i]; cmd.Op {
87		case "clip-push", "clip-pop":
88		case "text", "preedit-text":
89			for j := range cmd.Runs {
90				n += len(cmd.Runs[j].Glyphs)
91			}
92		default:
93			n++
94		}
95	}
96	return min(n, maxQuadPrealloc)
97}
98
99// Prepare converts logical rectangles once, using layout.Physical as the sole
100// logical-to-physical edge-rounding boundary. A clip-push intersects its parent
101// clip; rounded clips intentionally use only their axis-aligned bounds in V1.
102// Clip-pop without a push and unknown operations are rejected, not dropped.
103// Image commands without a source are rejected. Failed frames do not produce
104// partial output.
105func (p *Preparer) Prepare(commands []layout.Command, scale float64, width, height int) (Frame, error) {
106	if p == nil || p.Atlas == nil || scale <= 0 || math.IsNaN(scale) || math.IsInf(scale, 0) || width <= 0 || height <= 0 {
107		return Frame{}, fmt.Errorf("render: invalid frame dimensions, scale, or atlas")
108	}
109	p.Atlas.BeginFrame()
110	frame := Frame{}
111	if n := quadCapacity(commands); n > 0 {
112		// Frames are returned detached and may outlive later Prepare calls, so
113		// the backing array is never reused. Bound the initial reservation;
114		// larger visible frames grow through append after culling.
115		frame.Quads = make([]Quad, 0, n)
116	}
117	clip := layout.Rect{W: float64(width), H: float64(height)}
118	stack := []layout.Rect{}
119	for _, cmd := range commands {
120		switch cmd.Op {
121		case "clip-push":
122			stack = append(stack, clip)
123			clip = intersect(clip, physical(cmd.Rect, scale))
124			continue
125		case "clip-pop":
126			if len(stack) == 0 {
127				return Frame{}, fmt.Errorf("render: unbalanced clip-pop")
128			}
129			clip = stack[len(stack)-1]
130			stack = stack[:len(stack)-1]
131			continue
132		case "text", "preedit-text":
133			for _, run := range cmd.Runs {
134				if run.Face == nil || run.FaceID != run.Face.ID || run.Size <= 0 {
135					return Frame{}, fmt.Errorf("render: missing or mismatched face for %q", cmd.ID)
136				}
137				for _, glyph := range run.Glyphs {
138					x, y := glyph.X*scale, glyph.Y*scale
139					key, err := text.GlyphKey(run.Face, font.GID(glyph.ID), run.Size*scale, x, run.Face.Variations)
140					if err != nil {
141						return Frame{}, err
142					}
143					place, ok := p.Atlas.Lookup(key)
144					if !ok {
145						mask, err := text.Rasterize(run.Face, key)
146						if err != nil {
147							return Frame{}, err
148						}
149						place, err = p.Atlas.Insert(key, mask)
150						if err != nil {
151							return Frame{}, err
152						}
153					}
154					if place.Page < 0 || place.Rect.Empty() {
155						continue
156					}
157					r := layout.Rect{X: float64(int(math.Floor(x)) + place.Origin.X), Y: float64(int(math.Floor(y)) + place.Origin.Y), W: float64(place.Rect.Dx()), H: float64(place.Rect.Dy())}
158					if intersect(clip, r).W > 0 && intersect(clip, r).H > 0 {
159						frame.Quads = append(frame.Quads, Quad{Op: "glyph", Rect: r, Clip: clip, Color: opacity(cmd.Color, cmd.Opacity), Glyph: place})
160					}
161				}
162			}
163			continue
164		case "image":
165			if cmd.Image == nil {
166				return Frame{}, fmt.Errorf("render: image %q has no pixel source", cmd.ID)
167			}
168		case "rect", "border", "outline", "shadow", "selection", "caret", "preedit-underline", "preedit-caret":
169		default:
170			return Frame{}, fmt.Errorf("render: unknown display operation %q", cmd.Op)
171		}
172		r := physical(cmd.Rect, scale)
173		shape := r
174		var shadow [4]float64
175		inset := false
176		if cmd.Op == "outline" {
177			// Layout already applies outline-offset to Rect; width grows outward.
178			w := cmd.Widths.Top * scale
179			r = layout.Rect{X: r.X - w, Y: r.Y - w, W: r.W + 2*w, H: r.H + 2*w}
180		}
181		if cmd.Op == "shadow" {
182			if cmd.Shadow == nil {
183				return Frame{}, fmt.Errorf("render: shadow %q has no parameters", cmd.ID)
184			}
185			sh := cmd.Shadow
186			shadow = [4]float64{sh.X.Value * scale, sh.Y.Value * scale, sh.Blur.Value * scale, sh.Spread.Value * scale}
187			inset = sh.Inset
188			if !inset {
189				// Three standard deviations cover the visible Gaussian tail.
190				pad := shadow[2]*1.5 + math.Max(0, shadow[3])
191				r = layout.Rect{X: r.X + shadow[0] - pad, Y: r.Y + shadow[1] - pad, W: r.W + 2*pad, H: r.H + 2*pad}
192			}
193		}
194		if intersect(clip, r).W == 0 || intersect(clip, r).H == 0 {
195			continue
196		}
197		color := cmd.Color
198		if cmd.Op == "shadow" {
199			color = cmd.Shadow.Color
200		}
201		q := Quad{Op: cmd.Op, Rect: r, Clip: clip, Color: opacity(color, cmd.Opacity), Widths: scaled(cmd.Widths, scale), Colors: cmd.Colors, Shape: shape, Shadow: shadow, Inset: inset, Image: cmd.Image}
202		if cmd.Op == "image" {
203			q.Color = css.Color{R: 1, G: 1, B: 1, A: opacity(css.Color{A: 1}, cmd.Opacity).A}
204		}
205		for i, v := range cmd.Radii {
206			q.Radii[i] = v * scale
207		}
208		frame.Quads = append(frame.Quads, q)
209	}
210	if len(stack) > 0 {
211		return Frame{}, fmt.Errorf("render: unbalanced clip-push")
212	}
213	if p.owed {
214		// The previous frame's uploads may never have reached the GPU. Full pages
215		// subsume them and any changes made since.
216		p.Atlas.MarkAllDirty()
217	}
218	frame.Uploads = p.Atlas.Uploads()
219	p.issued, p.owed = frame.Uploads, len(frame.Uploads) > 0
220	return frame, nil
221}
222
223// Submitted acknowledges that uploads, as returned in the most recent Prepare's
224// Frame.Uploads, were recorded in a successfully submitted GPU submission.
225// Any other slice (an older, superseded frame or foreign data) is ignored and
226// the latest frame's uploads stay owed. Call it on the Prepare owner's loop.
227func (p *Preparer) Submitted(uploads []text.Upload) {
228	if p == nil || len(uploads) == 0 || len(uploads) != len(p.issued) || &uploads[0] != &p.issued[0] {
229		return
230	}
231	p.owed, p.issued = false, nil
232}
233
234func opacity(c css.Color, a float64) css.Color {
235	c.A *= math.Max(0, math.Min(1, a))
236	return c
237}

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