v0.4.0 2de4e1d

outputs.go RawHistoryFolder

1package ui
2
3import (
4	"cmp"
5	"context"
6	"errors"
7	"fmt"
8	"os"
9	"slices"
10	"time"
11
12	"github.com/bnema/nefergui/internal/css"
13	"github.com/bnema/nefergui/internal/platform/wayland"
14	"github.com/bnema/nefergui/internal/presentation/session"
15	"github.com/bnema/nefergui/internal/render"
16	"github.com/bnema/nefergui/internal/text"
17)
18
19// outputSurface is one layer surface and its own layout runtime: outputs
20// differ in size and scale, so each lays the shared view out separately.
21type outputSurface struct {
22	w      *wayland.Window
23	output uint32
24	r      *runtime
25	s      *session.Session
26	p      *session.Pump
27}
28
29// outputSet runs one layer surface per output on a single connection and
30// owner loop, following the session-lock structure. Outputs added later gain a
31// surface; removed outputs, or surfaces the compositor closes, lose theirs.
32type outputSet struct {
33	ctx         context.Context
34	conn        *wayland.Connection
35	opts        wayland.SurfaceOptions
36	width       int32
37	height      int32
38	transparent bool
39	sheet       css.Sheet
40	catalog     *text.Catalog
41	view        func(*Frame)
42	hooks       *inputHooks
43	surfaces    map[*wayland.Window]*outputSurface
44	pending     map[*wayland.Window]uint32 // created, waiting for configure and feedback
45	covered     map[uint32]bool            // outputs with a surface or a pending one
46	gpuWake     chan struct{}
47}
48
49// runAllOutputs is Run with LayerConfig.AllOutputs. It returns on cancellation
50// or the first failure; it never ends because outputs come and go.
51func runAllOutputs(ctx context.Context, cfg windowConfig, view func(*Frame)) (err error) {
52	if cfg.onSurface != nil || cfg.onResize != nil {
53		return errors.New("nefergui: AllOutputs excludes OnSurface and OnResize")
54	}
55	opts, err := cfg.layer.surfaceOptions()
56	if err != nil {
57		return err
58	}
59	a := &outputSet{ctx: ctx, opts: opts, width: int32(cfg.width), height: int32(cfg.height), transparent: cfg.transparent, view: view,
60		hooks: &inputHooks{onInput: cfg.onInput}, surfaces: map[*wayland.Window]*outputSurface{}, pending: map[*wayland.Window]uint32{},
61		covered: map[uint32]bool{}, gpuWake: make(chan struct{}, 1)}
62	if cfg.styles != "" {
63		data, e := os.ReadFile(cfg.styles)
64		if e != nil {
65			return fmt.Errorf("nefergui: styles: %w", e)
66		}
67		a.sheet = css.Parse(string(data))
68	}
69	if a.catalog, err = text.Load(text.SystemSource{}); err != nil {
70		return fmt.Errorf("nefergui: fonts: %w", err)
71	}
72	if len(a.catalog.Faces) == 0 {
73		return errors.New("nefergui: no usable fonts")
74	}
75	if a.conn, err = wayland.ConnectConnection(ctx, ""); err != nil {
76		return fmt.Errorf("nefergui: open platform: %w", err)
77	}
78	defer func() {
79		a.closeAll()
80		err = errors.Join(err, a.conn.Close())
81	}()
82	// Surfaces for the outputs present now are configured before the reader
83	// starts; output events seen meanwhile are held and handled right after.
84	if err = a.reconcile(); err != nil {
85		return err
86	}
87	events := a.conn.StartReader()
88	for _, ev := range a.conn.TakeHeld() {
89		if err = a.handle(ev); err != nil {
90			return err
91		}
92	}
93	ext := make(chan struct{}, 1)
94	if cfg.wake != nil {
95		loopCtx, stop := context.WithCancel(ctx)
96		defer stop()
97		go forward(loopCtx, cfg.wake, ext)
98	}
99	return a.loop(events, ext)
100}
101
102// forward relays external wake tokens to the owner loop until ctx ends or ch
103// closes; tokens coalesce.
104func forward(ctx context.Context, ch <-chan struct{}, out chan<- struct{}) {
105	for {
106		select {
107		case <-ctx.Done():
108			return
109		case _, ok := <-ch:
110			if !ok {
111				return
112			}
113			select {
114			case out <- struct{}{}:
115			default:
116			}
117		}
118	}
119}
120
121func (a *outputSet) loop(events <-chan wayland.Event, ext <-chan struct{}) error {
122	for {
123		if err := a.promote(); err != nil {
124			return err
125		}
126		// Every Redraw runs on this goroutine, so draining each runtime's own
127		// wake here sees every request, including Build follow-ups.
128		a.drainWakes()
129		retry := false
130		for _, o := range a.surfaces {
131			again, err := o.p.Step()
132			if err != nil {
133				return err
134			}
135			retry = retry || again
136		}
137		if a.drainWakes() {
138			continue // a Build asked for a follow-up frame
139		}
140		var retryC <-chan time.Time
141		if retry {
142			retryC = time.After(5 * time.Millisecond)
143		}
144		select {
145		case <-a.ctx.Done():
146			return a.ctx.Err()
147		case <-ext:
148			for _, o := range a.surfaces {
149				o.r.Redraw()
150			}
151		case <-a.gpuWake:
152		case <-retryC:
153		case ev := <-events:
154			if err := a.handle(ev); err != nil {
155				return err
156			}
157		}
158	}
159}
160
161// drainWakes invalidates every surface whose runtime requested a frame and
162// reports whether any did.
163func (a *outputSet) drainWakes() bool {
164	woke := false
165	for _, o := range a.surfaces {
166		select {
167		case <-o.r.wake:
168			o.p.Invalidate()
169			woke = true
170		default:
171		}
172	}
173	return woke
174}
175
176func (a *outputSet) handle(ev wayland.Event) error {
177	switch ev.Kind {
178	case wayland.BufferRelease:
179		if o := a.surfaces[ev.Window]; o != nil {
180			return o.p.BufferReleased(ev.BufferID)
181		}
182		return nil
183	case wayland.OutputRemoved:
184		a.removeOutput(ev.Output.Global)
185	}
186	if err := a.conn.Apply(ev); err != nil {
187		return err
188	}
189	if w := ev.Window; w != nil && w.Closed {
190		// The compositor closed this surface (layer_surface.closed); the output
191		// stays covered so it is not recreated in a loop.
192		a.closeWindow(w)
193		return nil
194	}
195	switch ev.Kind {
196	case wayland.OutputGlobal, wayland.OutputNamed, wayland.OutputAdded:
197		return a.reconcile()
198	}
199	// Seat changes queue input (leave, focus-out) on every window.
200	for _, o := range a.surfaces {
201		if err := a.input(o); err != nil {
202			return err
203		}
204	}
205	return nil
206}
207
208// reconcile creates a layer surface for every named output without one.
209func (a *outputSet) reconcile() error {
210	for _, out := range a.conn.Outputs() {
211		if a.covered[out.Global] {
212			continue
213		}
214		a.covered[out.Global] = true
215		l := *a.opts.Layer
216		l.OutputGlobal = out.Global
217		opts := a.opts
218		opts.Layer = &l
219		w, err := a.conn.NewWindow(a.ctx, a.width, a.height, a.transparent, opts)
220		if err != nil {
221			return fmt.Errorf("nefergui: layer surface for output %s: %w", out.Name, err)
222		}
223		a.pending[w] = out.Global
224	}
225	return nil
226}
227
228// promote opens the render path of every configured window with complete
229// dmabuf feedback, in ascending output order.
230func (a *outputSet) promote() error {
231	ready := make([]*wayland.Window, 0, len(a.pending))
232	for w := range a.pending {
233		if w.Configured && w.FeedbackDone {
234			ready = append(ready, w)
235		}
236	}
237	slices.SortFunc(ready, func(x, y *wayland.Window) int { return cmp.Compare(a.pending[x], a.pending[y]) })
238	for _, w := range ready {
239		output := a.pending[w]
240		delete(a.pending, w)
241		if w.Closed { // closed by the compositor before the reader started
242			_ = w.Close()
243			continue
244		}
245		o, err := a.open(w, output) // closes w on failure
246		if err != nil {
247			return fmt.Errorf("nefergui: layer render path: %w", err)
248		}
249		a.surfaces[w] = o
250	}
251	return nil
252}
253
254func (a *outputSet) open(w *wayland.Window, output uint32) (*outputSurface, error) {
255	s, err := session.OpenWindow(w, a.transparent)
256	if err != nil {
257		return nil, err
258	}
259	r := newRuntime()
260	r.styles = css.Compile(css.UA(), a.sheet)
261	r.setTextEngine(text.NewEngine(a.catalog))
262	o := &outputSurface{w: w, output: output, r: r, s: s}
263	if o.p, err = s.NewPump(a.ctx, a.gpuWake, func() (render.Frame, bool, error) { return a.draw(o) }); err != nil {
264		_ = s.Close()
265		return nil, err
266	}
267	return o, nil
268}
269
270func (a *outputSet) draw(o *outputSurface) (render.Frame, bool, error) {
271	w := o.w
272	o.r.route(platformInput{Kind: "resize", Width: float64(w.Width), Height: float64(w.Height), Scale: w.Scale})
273	if !o.r.Build(a.view) {
274		return render.Frame{}, false, nil
275	}
276	if o.r.consumedInput() {
277		// Control events may have changed the shared model: the other outputs
278		// rebuild without events, so this cannot bounce back.
279		for _, other := range a.surfaces {
280			if other != o {
281				other.r.Redraw()
282			}
283		}
284	}
285	if err := o.r.applyInputRects(w.StageInputRects); err != nil {
286		return render.Frame{}, false, err
287	}
288	if err := w.SetCursor(cursorShape(o.r.cursor())); err != nil {
289		return render.Frame{}, false, err
290	}
291	pw, ph, err := w.PhysicalSize()
292	if err != nil {
293		return render.Frame{}, false, err
294	}
295	frame, err := o.s.Preparer.Prepare(o.r.output.Display, w.Scale, int(pw), int(ph))
296	return frame, true, err
297}
298
299// input routes pointer input to the surface's runtime. A raw hook that
300// changed the model redraws every output, since they share it.
301func (a *outputSet) input(o *outputSurface) error {
302	inputs := o.w.DrainInput(nil)
303	if len(inputs) == 0 {
304		return nil
305	}
306	for _, in := range inputs {
307		switch {
308		case in.Enter:
309			o.w.PointerEntered(in.Serial, in.PointerGen)
310		case in.Kind == "leave":
311			o.w.PointerLeft()
312		}
313		if a.hooks.input(in) {
314			for _, other := range a.surfaces {
315				other.r.Redraw()
316			}
317		}
318		o.r.routeInput(in)
319	}
320	o.p.Invalidate()
321	return o.w.SetCursor(cursorShape(o.r.cursor()))
322}
323
324func (a *outputSet) removeOutput(global uint32) {
325	delete(a.covered, global)
326	for w, output := range a.pending {
327		if output == global {
328			delete(a.pending, w)
329			_ = w.Close()
330		}
331	}
332	for w, o := range a.surfaces {
333		if o.output == global {
334			delete(a.surfaces, w)
335			o.close()
336		}
337	}
338}
339
340func (a *outputSet) closeWindow(w *wayland.Window) {
341	if _, ok := a.pending[w]; ok {
342		delete(a.pending, w)
343		_ = w.Close()
344	}
345	if o := a.surfaces[w]; o != nil {
346		delete(a.surfaces, w)
347		o.close()
348	}
349}
350
351func (o *outputSurface) close() {
352	o.p.Close()
353	_ = o.s.Close()
354}
355
356func (a *outputSet) closeAll() {
357	for w, o := range a.surfaces {
358		delete(a.surfaces, w)
359		o.close()
360	}
361	for w := range a.pending {
362		delete(a.pending, w)
363		_ = w.Close()
364	}
365}

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