1// Command layer shows a wlr-layer-shell surface with a counter button: the
2// smallest end-to-end use of neferclient (Wayland client) and NeferGUI
3// (renderer). The two libraries do not import each other; this file is the
4// glue, and every step is marked.
5//
6// Everything runs on one owner goroutine: the loop in run waits for a wake-up,
7// calls Conn.Dispatch, and all Handler methods, renderer calls and Present
8// calls happen inside that call or right after it.
9//
10// Only the button takes pointer input: the view stages its rectangle with
11// Frame.SetInputRects and the program forwards it to Surface.SetInputRegion,
12// so the rest of the surface is click-through.
13//
14// With -frames N the program redraws continuously and exits after N frames
15// (used by the headless test); without it, it runs until the surface is closed
16// or it is interrupted. -log-pointer prints pointer enter and leave events.
17package main
18
19import (
20 "context"
21 "errors"
22 "flag"
23 "fmt"
24 "os"
25 "os/signal"
26 "syscall"
27 "time"
28
29 "github.com/bnema/neferclient"
30 "github.com/bnema/nefergui"
31)
32
33// Layout of the surface, in logical pixels: padding, a label row, a gap, then
34// the button. The headless test samples a pixel inside the button and moves the
35// pointer over the label and the button at the same places.
36const (
37 surfaceW, surfaceH = 240, 90
38 pad, labelH, gap = 10, 20, 10
39 buttonW, buttonH = 200, 40
40 buttonY = pad + labelH + gap
41 buttonColor = "#2060c0"
42
43 // guiMods is the set of modifiers NeferGUI knows. neferclient and NeferGUI
44 // use the same bit order for them, so the plain bits can be copied.
45 guiMods = neferclient.ModShift | neferclient.ModCtrl | neferclient.ModAlt | neferclient.ModSuper
46)
47
48type model struct{ count int }
49
50// The styles and the clickable rectangle are built once from the layout
51// constants, so they cannot disagree. NeferGUI does not report where a control
52// was laid out; inputRects is the button. SetInputRects copies the slice.
53var (
54 columnStyle = nefergui.Inline(fmt.Sprintf("padding:%dpx;gap:%dpx;color:#ffffff", pad, gap))
55 labelStyle = nefergui.Inline(fmt.Sprintf("height:%dpx", labelH))
56 buttonStyle = nefergui.Inline(fmt.Sprintf("width:%dpx;height:%dpx;background:%s", buttonW, buttonH, buttonColor))
57 inputRects = []nefergui.Rect{{X: pad, Y: buttonY, Width: buttonW, Height: buttonH}}
58)
59
60// view builds one frame from the model. Button.Activated is true during the
61// frame built after a click.
62func view(f *nefergui.Frame, m *model) {
63 root := f.Root().Column(columnStyle)
64 root.TextInt("Count: ", int64(m.count), labelStyle)
65 if root.Button("Add one", nefergui.Key("add"), buttonStyle).Activated() {
66 m.count++
67 }
68 if err := f.SetInputRects(inputRects); err != nil {
69 panic(err) // a fixed, valid rectangle: an error is a programming bug
70 }
71}
72
73type app struct {
74 neferclient.NopHandler // methods this program does not need
75
76 conn *neferclient.Conn
77 surf *neferclient.Surface
78 seat *neferclient.Seat
79
80 r *nefergui.Renderer
81 rwake <-chan struct{} // Renderer.Wake: nil until the renderer exists
82 out nefergui.Output
83 model model
84 damage []neferclient.Rect
85 region []neferclient.Rect // input region sent to the compositor, reused
86
87 logPointer bool
88
89 configured bool
90 canPresent bool // configured, and the last frame callback fired
91 haveAcquire bool
92 cursor neferclient.CursorShape
93
94 frames, shown int // -frames limit and frames acknowledged so far
95 done bool
96 err error
97}
98
99func main() {
100 frames := flag.Int("frames", 0, "redraw continuously and exit after this many frames (0: run until closed)")
101 logPointer := flag.Bool("log-pointer", false, "print pointer enter and leave events on stdout")
102 flag.Parse()
103 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
104 defer stop()
105 if err := run(ctx, *frames, *logPointer); err != nil {
106 fmt.Fprintln(os.Stderr, "layer:", err)
107 os.Exit(1)
108 }
109}
110
111func run(ctx context.Context, frames int, logPointer bool) (err error) {
112 conn, err := neferclient.Connect(ctx, "") // WAYLAND_DISPLAY
113 if err != nil {
114 return fmt.Errorf("connect: %w", err)
115 }
116 // region starts non-nil so that an empty NeferGUI region stays an empty
117 // slice (click-through) and does not become nil (whole surface).
118 a := &app{conn: conn, frames: frames, logPointer: logPointer, cursor: neferclient.CursorDefault,
119 region: []neferclient.Rect{}}
120 // Shutdown order: close the connection first, which drops its descriptor
121 // watches and destroys every Wayland object, then free the renderer and
122 // its GPU resources (including the release eventfds).
123 defer func() {
124 err = errors.Join(err, conn.Close())
125 if a.r != nil {
126 err = errors.Join(err, a.r.Close())
127 }
128 }()
129 a.surf, err = conn.NewLayerSurface(neferclient.LayerConfig{
130 Level: neferclient.LayerTop,
131 Anchors: neferclient.AnchorTop | neferclient.AnchorLeft,
132 Width: surfaceW,
133 Height: surfaceH,
134 Keyboard: neferclient.KeyboardOnDemand,
135 })
136 if err != nil {
137 return fmt.Errorf("layer surface: %w", err)
138 }
139 a.seat = conn.Seat()
140
141 var tick <-chan time.Time
142 for !a.done {
143 select {
144 case <-ctx.Done():
145 return nil // interrupt or SIGTERM: a clean exit
146 case <-conn.Wake():
147 if err = conn.Dispatch(a); err != nil {
148 return fmt.Errorf("dispatch: %w", err)
149 }
150 case <-a.rwake: // another goroutine asked for a frame
151 case <-tick: // a built frame was waiting for the GPU
152 }
153 if a.err != nil {
154 return a.err
155 }
156 if err = a.draw(); err != nil {
157 return err
158 }
159 tick = nil
160 if a.r != nil && a.r.Pending() {
161 tick = time.After(2 * time.Millisecond)
162 }
163 }
164 return nil
165}
166
167// FeedbackDone: dmabuf feedback → nefergui.RendererConfig. The renderer opens
168// the compositor's main device and picks a format and modifier from the list.
169//
170// Only the first complete feedback is used. A later one (the compositor
171// changing its preferred device or formats) is ignored: this demo assumes a
172// single GPU, and rebuilding the renderer is application policy that would
173// obscure the glue. A real application would create a new Renderer and
174// re-import its buffers here.
175func (a *app) FeedbackDone(neferclient.SurfaceID) { a.setup() }
176
177func (a *app) Configure(neferclient.SurfaceID, int32, int32) {
178 if !a.configured { // later configures must not lift the frame-callback gate
179 a.canPresent = true
180 }
181 a.configured = true
182 a.resize()
183 a.setup()
184}
185
186func (a *app) Scale(neferclient.SurfaceID, float64) { a.resize() }
187
188func (a *app) setup() {
189 fb := a.surf.Feedback()
190 if a.r != nil || !a.configured || fb == nil {
191 return
192 }
193 cfg := nefergui.RendererConfig{MainDevice: fb.MainDevice, Formats: make([]nefergui.Format, len(fb.Formats))}
194 for i, f := range fb.Formats {
195 cfg.Formats[i] = nefergui.Format{FourCC: f.FourCC, Modifier: f.Modifier}
196 }
197 r, err := nefergui.NewRenderer(cfg)
198 if err != nil {
199 a.fail(fmt.Errorf("renderer: %w", err))
200 return
201 }
202 a.r, a.rwake = r, r.Wake()
203 a.resize()
204}
205
206// resize forwards the logical size and scale to the renderer.
207func (a *app) resize() {
208 if a.r == nil {
209 return
210 }
211 w, h, scale := a.surf.Size()
212 a.r.Resize(int(w), int(h), scale)
213}
214
215// Frame: the compositor showed the last commit; the next Present is allowed.
216func (a *app) Frame(neferclient.SurfaceID) {
217 a.canPresent = true
218 a.shown++
219 if a.frames > 0 {
220 if a.shown >= a.frames {
221 a.done = true
222 return
223 }
224 a.r.Invalidate()
225 }
226}
227
228func (a *app) Closed(neferclient.SurfaceID) { a.done = true }
229
230func (a *app) Error(err error) { a.fail(err) }
231
232func (a *app) fail(err error) {
233 if a.err == nil {
234 a.err = err
235 }
236}
237
238// FDReady: the release eventfd of a buffer is readable (watched with
239// Conn.WatchFD in present, id is the buffer): hand the buffer back.
240func (a *app) FDReady(id uint64) {
241 if err := a.r.Released(id); err != nil {
242 a.fail(fmt.Errorf("release buffer %d: %w", id, err))
243 }
244}
245
246// Pointer and Key: neferclient events → nefergui.Input, field by field.
247func (a *app) Pointer(ev *neferclient.PointerEvent) {
248 if a.r == nil {
249 return
250 }
251 in := nefergui.Input{X: ev.X, Y: ev.Y}
252 switch ev.Kind {
253 case neferclient.PointerEnter:
254 in.Kind = nefergui.InputPointerMotion
255 if a.logPointer {
256 fmt.Printf("pointer enter %.0f %.0f\n", ev.X, ev.Y)
257 }
258 // The cursor shape is only valid after the enter: set the shape of
259 // the last frame again.
260 if err := a.seat.SetCursor(a.cursor); err != nil {
261 a.fail(fmt.Errorf("cursor: %w", err))
262 }
263 case neferclient.PointerMotion:
264 in.Kind = nefergui.InputPointerMotion
265 case neferclient.PointerLeave:
266 in.Kind = nefergui.InputPointerLeave
267 if a.logPointer {
268 fmt.Println("pointer leave")
269 }
270 case neferclient.PointerButton:
271 in.Kind, in.Button = nefergui.InputPointerRelease, ev.Button
272 if ev.Pressed {
273 in.Kind = nefergui.InputPointerPress
274 }
275 case neferclient.PointerAxis:
276 in.Kind, in.DX, in.DY = nefergui.InputPointerAxis, ev.DX, ev.DY
277 default:
278 return
279 }
280 a.r.Input(&in)
281}
282
283func (a *app) Key(ev *neferclient.KeyEvent) {
284 if a.r == nil {
285 return
286 }
287 a.r.Input(&nefergui.Input{
288 Kind: nefergui.InputKey,
289 Keysym: ev.Keysym,
290 Text: ev.Text, // valid only during the call, as for Input
291 Pressed: ev.Pressed,
292 Repeat: ev.Repeat,
293 Modifiers: nefergui.Modifiers(ev.Modifiers & guiMods),
294 })
295}
296
297func (a *app) KeyboardFocus(_ neferclient.SurfaceID, focused bool) {
298 if a.r == nil {
299 return
300 }
301 kind := nefergui.InputFocusOut
302 if focused {
303 kind = nefergui.InputFocusIn
304 }
305 a.r.Input(&nefergui.Input{Kind: kind})
306}
307
308// draw renders a frame when something changed and presents it.
309func (a *app) draw() error {
310 if a.r == nil || !a.canPresent {
311 return nil
312 }
313 ok, err := a.r.Render(&a.out, &a.model, view)
314 if err != nil {
315 return fmt.Errorf("render: %w", err)
316 }
317 if !ok {
318 return nil
319 }
320 if err = a.setInputRegion(); err != nil {
321 return err
322 }
323 if err = a.present(); err != nil {
324 return err
325 }
326 return a.setCursor()
327}
328
329// setInputRegion: Output.InputRects → Surface.SetInputRegion, only when the
330// view changed it. Both use logical pixels in surface coordinates, so the
331// rectangles are copied field by field without scaling (Output.Width and
332// Height, the buffer size, are the physical ones). Nil means the whole surface
333// and an empty slice means click-through, in both libraries. Before the first
334// Present the region is applied with it; afterwards SetInputRegion commits it
335// immediately.
336func (a *app) setInputRegion() error {
337 out := &a.out
338 if !out.InputRectsChanged {
339 return nil
340 }
341 var region []neferclient.Rect // nil: the whole surface
342 if out.InputRects != nil {
343 a.region = a.region[:0] // reused: no allocation once it has grown
344 for _, r := range out.InputRects {
345 a.region = append(a.region, neferclient.Rect{X: r.X, Y: r.Y, Width: r.Width, Height: r.Height})
346 }
347 region = a.region
348 }
349 if err := a.surf.SetInputRegion(region); err != nil {
350 return fmt.Errorf("input region: %w", err)
351 }
352 return nil
353}
354
355// present: Renderer.Render output → ImportBuffer, ImportTimeline, Present.
356func (a *app) present() error {
357 out := &a.out
358 // A resize retired buffers: stop watching and destroy them first.
359 for _, rt := range out.Retired {
360 if err := a.conn.UnwatchFD(rt.ReleaseFD); err != nil {
361 return fmt.Errorf("unwatch retired buffer %d: %w", rt.Buffer, err)
362 }
363 if err := a.surf.DestroyBuffer(rt.Buffer); err != nil {
364 return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err)
365 }
366 // The release timeline of a buffer is imported under the buffer's id.
367 if err := a.surf.DestroyTimeline(rt.Buffer); err != nil {
368 return fmt.Errorf("destroy retired timeline %d: %w", rt.Buffer, err)
369 }
370 }
371 if out.NewBuffer {
372 buf := neferclient.Buffer{
373 Width: out.Width, Height: out.Height, FourCC: out.FourCC, Modifier: out.Modifier,
374 PlaneCount: out.PlaneCount,
375 }
376 for i, p := range out.Planes {
377 buf.Planes[i] = neferclient.Plane{FD: p.FD, Offset: p.Offset, Stride: p.Stride}
378 }
379 if err := a.surf.ImportBuffer(out.Buffer, &buf); err != nil {
380 return fmt.Errorf("import buffer: %w", err)
381 }
382 // The release eventfd is stable per buffer: watch it once; FDReady
383 // calls Renderer.Released.
384 if err := a.conn.WatchFD(out.ReleaseFD, out.Buffer); err != nil {
385 return fmt.Errorf("watch release fd: %w", err)
386 }
387 }
388 if out.NewTimelines {
389 if !a.haveAcquire { // the acquire timeline is shared by every buffer
390 if err := a.surf.ImportTimeline(out.Acquire.ID, out.Acquire.FD); err != nil {
391 return fmt.Errorf("import acquire timeline: %w", err)
392 }
393 a.haveAcquire = true
394 }
395 if err := a.surf.ImportTimeline(out.Release.ID, out.Release.FD); err != nil {
396 return fmt.Errorf("import release timeline: %w", err)
397 }
398 }
399 a.damage = a.damage[:0]
400 for _, d := range out.Damage {
401 a.damage = append(a.damage, neferclient.Rect{X: d.X, Y: d.Y, Width: d.Width, Height: d.Height})
402 }
403 err := a.surf.Present(&neferclient.Present{
404 Buffer: out.Buffer,
405 AcquireTimeline: out.Acquire.ID,
406 ReleaseTimeline: out.Release.ID,
407 AcquirePoint: out.AcquirePoint,
408 ReleasePoint: out.ReleasePoint,
409 Damage: a.damage,
410 Opaque: true, // XRGB8888: RendererConfig.Transparent is false
411 })
412 if err != nil {
413 return fmt.Errorf("present: %w", err)
414 }
415 a.canPresent = false // until Frame
416 return nil
417}
418
419// setCursor: the cursor the hovered element asked for → Seat.SetCursor.
420func (a *app) setCursor() error {
421 switch a.out.Cursor {
422 case nefergui.CursorPointer:
423 a.cursor = neferclient.CursorPointer
424 case nefergui.CursorText:
425 a.cursor = neferclient.CursorText
426 case nefergui.CursorNotAllowed:
427 a.cursor = neferclient.CursorNotAllowed
428 default:
429 a.cursor = neferclient.CursorDefault
430 }
431 if err := a.seat.SetCursor(a.cursor); err != nil {
432 return fmt.Errorf("cursor: %w", err)
433 }
434 return nil
435}