1// Package layerui shows one NeferGUI view on a layer surface: neferclient owns
2// the Wayland connection and surface, nefergui.Renderer draws into DMA-BUF
3// buffers, and this package copies plain fields between the two. Each Run has
4// its own connection and runs everything on the calling goroutine.
5package layerui
6
7import (
8 "context"
9 "errors"
10 "fmt"
11 "time"
12
13 "github.com/bnema/neferclient"
14 "github.com/bnema/nefergui"
15 "github.com/bnema/wlturbo"
16)
17
18// guiMods is the set of modifiers NeferGUI knows. Both libraries use the same
19// bit order, so the plain bits can be copied.
20const guiMods = neferclient.ModShift | neferclient.ModCtrl | neferclient.ModAlt | neferclient.ModSuper |
21 neferclient.ModCapsLock | neferclient.ModNumLock
22
23// pendingRetry is how soon a frame that waits for the GPU is tried again.
24const pendingRetry = 2 * time.Millisecond
25
26// Config describes one layer surface and the hooks of its owner loop. Every
27// hook but OnSurface runs on the goroutine that called Run.
28type Config struct {
29 Layer neferclient.LayerConfig
30 Styles string // CSS file path for nefergui.RendererConfig.Styles
31
32 // Wake redraws the view for each value received.
33 Wake <-chan struct{}
34 // OnResize reports the logical size and scale at the first configure and
35 // on every change.
36 OnResize func(width, height int, scale float64)
37 // OnInput sees every input event before NeferGUI routes it to controls;
38 // returning true redraws the view.
39 OnInput func(nefergui.Input) bool
40 // OnSurface runs once, after the layer is configured and before its first
41 // buffer is shown, with the connection's display and the layer's
42 // wl_surface. An error ends Run and nothing is shown. It runs on its own
43 // goroutine while the owner loop keeps applying events, so it may wait for
44 // the compositor's answer to a request of its own; it must only send
45 // requests, and return when ctx ends: Run waits for it.
46 //
47 // The input region starts as Layer.InputRects; a view's
48 // Frame.SetInputRects replaces it.
49 OnSurface func(ctx context.Context, display *wlturbo.Display, surface wlturbo.Proxy) error
50}
51
52// Run shows view on a transparent layer surface until ctx ends (it returns
53// ctx.Err()), the compositor closes the surface (nil) or something fails.
54func Run[T any](ctx context.Context, cfg Config, model *T, view func(*nefergui.Frame, *T)) (err error) {
55 conn, err := neferclient.Connect(ctx, "") // WAYLAND_DISPLAY
56 if err != nil {
57 return fmt.Errorf("connect: %w", err)
58 }
59 l := &loop[T]{ctx: ctx, cfg: cfg, conn: conn, model: model, view: view, cursor: neferclient.CursorDefault}
60 // Close the connection first (it destroys every Wayland object), then
61 // free the renderer and its GPU resources.
62 defer func() {
63 err = errors.Join(err, conn.Close())
64 if l.r != nil {
65 err = errors.Join(err, l.r.Close())
66 }
67 }()
68 if l.surf, err = conn.NewLayerSurface(cfg.Layer); err != nil {
69 return fmt.Errorf("layer surface: %w", err)
70 }
71 l.seat = conn.Seat()
72 defer l.waitHook()
73 return l.run()
74}
75
76type loop[T any] struct {
77 neferclient.NopHandler
78
79 ctx context.Context
80 cfg Config
81 conn *neferclient.Conn
82 surf *neferclient.Surface
83 seat *neferclient.Seat
84 model *T
85 view func(*nefergui.Frame, *T)
86
87 r *nefergui.Renderer
88 rwake <-chan struct{} // Renderer.Wake: nil until the renderer exists
89 out nefergui.Output
90 damage []neferclient.Rect
91 region []neferclient.Rect // reused by setInputRegion
92
93 w, h int32
94 scale float64
95 configured bool
96 canPresent bool // configured, and the last frame callback fired
97 authorized bool // OnSurface succeeded, or there is none
98 hookStarted bool
99 hookDone chan error // OnSurface's result while it is pending
100 hookCancel context.CancelFunc
101 haveAcquire bool
102 cursor neferclient.CursorShape
103
104 closed bool
105 err error
106}
107
108func (l *loop[T]) run() error {
109 l.authorized = l.cfg.OnSurface == nil
110 retry := time.NewTimer(pendingRetry) // reused: Pending can last many frames
111 retry.Stop()
112 defer retry.Stop()
113 var tick <-chan time.Time
114 for !l.closed {
115 select {
116 case <-l.ctx.Done():
117 return l.ctx.Err()
118 case <-l.conn.Wake():
119 if err := l.conn.Dispatch(l); err != nil {
120 return fmt.Errorf("dispatch: %w", err)
121 }
122 case _, ok := <-l.cfg.Wake:
123 if !ok {
124 l.cfg.Wake = nil // a closed channel would spin
125 } else if l.r != nil {
126 l.r.Invalidate()
127 }
128 case err := <-l.hookDone:
129 l.hookDone = nil
130 if err != nil {
131 return err
132 }
133 l.authorized = true
134 case <-l.rwake: // another goroutine asked for a frame
135 case <-tick: // a built frame was waiting for the GPU
136 }
137 if l.err != nil {
138 return l.err
139 }
140 if l.closed {
141 return nil
142 }
143 if err := l.draw(); err != nil {
144 return err
145 }
146 tick = nil
147 if l.r != nil && l.r.Pending() {
148 retry.Reset(pendingRetry)
149 tick = retry.C
150 }
151 }
152 return nil
153}
154
155// FeedbackDone: dmabuf feedback → nefergui.RendererConfig. Only the first
156// complete feedback is used: one GPU is assumed.
157func (l *loop[T]) FeedbackDone(neferclient.SurfaceID) { l.setup() }
158
159func (l *loop[T]) Configure(neferclient.SurfaceID, int32, int32) {
160 if !l.configured { // later configures must not lift the frame-callback gate
161 l.canPresent = true
162 }
163 l.configured = true
164 l.resize()
165 l.setup()
166}
167
168func (l *loop[T]) Scale(neferclient.SurfaceID, float64) { l.resize() }
169
170func (l *loop[T]) setup() {
171 fb := l.surf.Feedback()
172 if l.r != nil || !l.configured || fb == nil {
173 return
174 }
175 cfg := nefergui.RendererConfig{MainDevice: fb.MainDevice, Transparent: true, Styles: l.cfg.Styles,
176 Formats: make([]nefergui.Format, len(fb.Formats))}
177 for i, f := range fb.Formats {
178 cfg.Formats[i] = nefergui.Format{FourCC: f.FourCC, Modifier: f.Modifier}
179 }
180 r, err := nefergui.NewRenderer(cfg)
181 if err != nil {
182 l.fail(fmt.Errorf("renderer: %w", err))
183 return
184 }
185 l.r, l.rwake = r, r.Wake()
186 w, h, scale := l.surf.Size()
187 r.Resize(int(w), int(h), scale)
188}
189
190// resize forwards a changed logical size or scale to OnResize and the
191// renderer. Before the first configure the size is not known yet.
192func (l *loop[T]) resize() {
193 if !l.configured {
194 return
195 }
196 w, h, scale := l.surf.Size()
197 if w == l.w && h == l.h && scale == l.scale {
198 return
199 }
200 l.w, l.h, l.scale = w, h, scale
201 if l.cfg.OnResize != nil {
202 l.cfg.OnResize(int(w), int(h), scale)
203 }
204 if l.r != nil {
205 l.r.Resize(int(w), int(h), scale)
206 }
207}
208
209// Frame: the compositor showed the last commit; the next Present is allowed.
210func (l *loop[T]) Frame(neferclient.SurfaceID) { l.canPresent = true }
211
212func (l *loop[T]) Closed(neferclient.SurfaceID) { l.closed = true }
213
214func (l *loop[T]) Error(err error) { l.fail(err) }
215
216func (l *loop[T]) fail(err error) {
217 if l.err == nil {
218 l.err = err
219 }
220}
221
222// FDReady: a buffer's release eventfd is readable (id is the buffer).
223func (l *loop[T]) FDReady(id uint64) {
224 if err := l.r.Released(id); err != nil {
225 l.fail(fmt.Errorf("release buffer %d: %w", id, err))
226 }
227}
228
229func (l *loop[T]) Pointer(ev *neferclient.PointerEvent) {
230 in, ok := pointerInput(ev)
231 if !ok {
232 return
233 }
234 if ev.Kind == neferclient.PointerEnter {
235 // The cursor shape is only valid after the enter: set it again.
236 if err := l.seat.SetCursor(l.cursor); err != nil {
237 l.fail(fmt.Errorf("cursor: %w", err))
238 }
239 }
240 l.input(&in)
241}
242
243func (l *loop[T]) Key(ev *neferclient.KeyEvent) {
244 in := keyInput(ev)
245 l.input(&in)
246}
247
248func (l *loop[T]) KeyboardFocus(_ neferclient.SurfaceID, focused bool) {
249 in := nefergui.Input{Kind: nefergui.InputFocusOut}
250 if focused {
251 in.Kind = nefergui.InputFocusIn
252 }
253 l.input(&in)
254}
255
256// input gives ev to OnInput, then to NeferGUI. Before the renderer exists
257// nothing is shown, so nothing is delivered.
258func (l *loop[T]) input(ev *nefergui.Input) {
259 if l.r == nil {
260 return
261 }
262 if l.cfg.OnInput != nil && l.cfg.OnInput(*ev) {
263 l.r.Invalidate()
264 }
265 l.r.Input(ev)
266}
267
268// pointerInput converts a neferclient pointer event; ok is false for events
269// NeferGUI does not take.
270func pointerInput(ev *neferclient.PointerEvent) (in nefergui.Input, ok bool) {
271 in = nefergui.Input{X: ev.X, Y: ev.Y}
272 switch ev.Kind {
273 case neferclient.PointerEnter, neferclient.PointerMotion:
274 in.Kind = nefergui.InputPointerMotion
275 case neferclient.PointerLeave:
276 in.Kind = nefergui.InputPointerLeave
277 case neferclient.PointerButton:
278 in.Kind, in.Button, in.Pressed = nefergui.InputPointerRelease, ev.Button, ev.Pressed
279 if ev.Pressed {
280 in.Kind = nefergui.InputPointerPress
281 }
282 case neferclient.PointerAxis:
283 in.Kind, in.DX, in.DY = nefergui.InputPointerAxis, ev.DX, ev.DY
284 default:
285 return nefergui.Input{}, false
286 }
287 return in, true
288}
289
290// keyInput converts a neferclient key event. Text aliases neferclient storage
291// that is valid only during the Key call, as NeferGUI requires.
292func keyInput(ev *neferclient.KeyEvent) nefergui.Input {
293 return nefergui.Input{
294 Kind: nefergui.InputKey,
295 Keysym: ev.Keysym,
296 Text: ev.Text,
297 Pressed: ev.Pressed,
298 Repeat: ev.Repeat,
299 Modifiers: nefergui.Modifiers(ev.Modifiers & guiMods),
300 }
301}
302
303// draw renders a frame when something changed and presents it.
304func (l *loop[T]) draw() error {
305 if l.r == nil || !l.canPresent {
306 return nil
307 }
308 if !l.authorized {
309 l.startHook()
310 return nil
311 }
312 ok, err := l.r.Render(&l.out, l.model, l.view)
313 if err != nil {
314 return fmt.Errorf("render: %w", err)
315 }
316 if !ok {
317 return nil
318 }
319 if err = l.setInputRegion(); err != nil {
320 return err
321 }
322 if err = l.present(); err != nil {
323 return err
324 }
325 return l.setCursor()
326}
327
328// setInputRegion: Output.InputRects → Surface.SetInputRegion, only when the
329// view changed it. Both are logical surface pixels; nil means the whole
330// surface and an empty slice click-through in both libraries.
331func (l *loop[T]) setInputRegion() error {
332 region, changed := inputRegion(l.region[:0], &l.out)
333 if !changed {
334 return nil
335 }
336 if region != nil {
337 l.region = region
338 }
339 if err := l.surf.SetInputRegion(region); err != nil {
340 return fmt.Errorf("input region: %w", err)
341 }
342 return nil
343}
344
345// inputRegion converts out's input region into dst; changed is false when
346// the view did not change it.
347func inputRegion(dst []neferclient.Rect, out *nefergui.Output) (region []neferclient.Rect, changed bool) {
348 if !out.InputRectsChanged {
349 return nil, false
350 }
351 if out.InputRects == nil {
352 return nil, true
353 }
354 if dst == nil {
355 dst = []neferclient.Rect{}
356 }
357 for _, r := range out.InputRects {
358 dst = append(dst, neferclient.Rect{X: r.X, Y: r.Y, Width: r.Width, Height: r.Height})
359 }
360 return dst, true
361}
362
363// startHook starts OnSurface once. The owner loop keeps dispatching while it
364// waits: neferclient's reader stops reading when the event queue is full.
365func (l *loop[T]) startHook() {
366 if l.hookStarted {
367 return
368 }
369 l.hookStarted = true
370 ctx, cancel := context.WithCancel(l.ctx)
371 done := make(chan error, 1)
372 l.hookDone, l.hookCancel = done, cancel
373 display, surface := l.conn.Display(), l.surf.WLSurface()
374 go func() { done <- l.cfg.OnSurface(ctx, display, surface) }()
375}
376
377// waitHook ends a pending OnSurface and waits for it before the connection
378// closes.
379func (l *loop[T]) waitHook() {
380 if l.hookCancel == nil {
381 return
382 }
383 l.hookCancel()
384 if l.hookDone != nil {
385 <-l.hookDone
386 }
387}
388
389// present: Renderer.Render output → ImportBuffer, ImportTimeline, Present.
390func (l *loop[T]) present() error {
391 out := &l.out
392 // A resize retired buffers: stop watching and destroy them first.
393 for _, rt := range out.Retired {
394 if err := l.conn.UnwatchFD(rt.ReleaseFD); err != nil {
395 return fmt.Errorf("unwatch retired buffer %d: %w", rt.Buffer, err)
396 }
397 if err := l.surf.DestroyBuffer(rt.Buffer); err != nil {
398 return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err)
399 }
400 // The release timeline of a buffer is imported under the buffer's id.
401 if err := l.surf.DestroyTimeline(rt.Buffer); err != nil {
402 return fmt.Errorf("destroy retired timeline %d: %w", rt.Buffer, err)
403 }
404 }
405 if out.NewBuffer {
406 buf := neferclient.Buffer{
407 Width: out.Width, Height: out.Height, FourCC: out.FourCC, Modifier: out.Modifier,
408 PlaneCount: out.PlaneCount,
409 }
410 for i, p := range out.Planes {
411 buf.Planes[i] = neferclient.Plane{FD: p.FD, Offset: p.Offset, Stride: p.Stride}
412 }
413 if err := l.surf.ImportBuffer(out.Buffer, &buf); err != nil {
414 return fmt.Errorf("import buffer: %w", err)
415 }
416 // The release eventfd is stable per buffer: watch it once.
417 if err := l.conn.WatchFD(out.ReleaseFD, out.Buffer); err != nil {
418 return fmt.Errorf("watch release fd: %w", err)
419 }
420 }
421 if out.NewTimelines {
422 if !l.haveAcquire { // the acquire timeline is shared by every buffer
423 if err := l.surf.ImportTimeline(out.Acquire.ID, out.Acquire.FD); err != nil {
424 return fmt.Errorf("import acquire timeline: %w", err)
425 }
426 l.haveAcquire = true
427 }
428 if err := l.surf.ImportTimeline(out.Release.ID, out.Release.FD); err != nil {
429 return fmt.Errorf("import release timeline: %w", err)
430 }
431 }
432 l.damage = l.damage[:0]
433 for _, d := range out.Damage {
434 l.damage = append(l.damage, neferclient.Rect{X: d.X, Y: d.Y, Width: d.Width, Height: d.Height})
435 }
436 err := l.surf.Present(&neferclient.Present{
437 Buffer: out.Buffer,
438 AcquireTimeline: out.Acquire.ID,
439 ReleaseTimeline: out.Release.ID,
440 AcquirePoint: out.AcquirePoint,
441 ReleasePoint: out.ReleasePoint,
442 Damage: l.damage,
443 })
444 if err != nil {
445 return fmt.Errorf("present: %w", err)
446 }
447 l.canPresent = false // until Frame
448 return nil
449}
450
451// setCursor: the cursor the hovered element asked for → Seat.SetCursor.
452func (l *loop[T]) setCursor() error {
453 switch l.out.Cursor {
454 case nefergui.CursorPointer:
455 l.cursor = neferclient.CursorPointer
456 case nefergui.CursorText:
457 l.cursor = neferclient.CursorText
458 case nefergui.CursorNotAllowed:
459 l.cursor = neferclient.CursorNotAllowed
460 default:
461 l.cursor = neferclient.CursorDefault
462 }
463 if err := l.seat.SetCursor(l.cursor); err != nil {
464 return fmt.Errorf("cursor: %w", err)
465 }
466 return nil
467}