1// Package bar connects the Wayland layer surface, the Vulkan renderer and the
2// module layout.
3//
4// Everything runs on one goroutine: the loop in Run waits for a Wayland
5// wake-up or a module frame, calls Dispatch, and every Handler method,
6// renderer call and Present happens inside it. Modules only publish frames
7// through a channel.
8package bar
9
10import (
11 "context"
12 "errors"
13 "fmt"
14 "log/slog"
15 "math"
16 "os"
17 "path/filepath"
18 "slices"
19 "strings"
20 "syscall"
21 "time"
22
23 "github.com/bnema/neferclient"
24
25 "github.com/bnema/neferbar/internal/config"
26 "github.com/bnema/neferbar/internal/fswatch"
27 "github.com/bnema/neferbar/internal/glyph"
28 "github.com/bnema/neferbar/internal/gpu"
29 "github.com/bnema/neferbar/internal/layout"
30 "github.com/bnema/neferbar/internal/module"
31 "github.com/bnema/neferbar/internal/syncobj"
32 "github.com/bnema/neferbar/internal/theme"
33)
34
35const acquireTimelineID = 1 << 32
36
37// Stats counts what the bar did; read it only from the loop goroutine or after
38// Run returns.
39type Stats struct {
40 Presented uint64 // frames handed to the compositor
41 Rebuilds uint64 // renderer rebuilds (scale or size changes)
42 Recreated uint64 // layer surface recreations
43 NoSlot uint64 // draws skipped because every image was held
44 Dirty uint64 // module wake-ups that changed the row
45 ModuleWake uint64
46}
47
48// Bar is the running application.
49type Bar struct {
50 cfg config.Config
51 log *slog.Logger
52 display string
53 fg, bg [3]uint8
54
55 conn *neferclient.Conn
56 surf *neferclient.Surface
57 sid neferclient.SurfaceID
58
59 configured bool
60 canPresent bool
61 dirty bool
62
63 // font
64 fonts glyph.Paths
65 face *glyph.Face
66 facePx float64
67
68 dev *gpu.Device
69 node *syncobj.Node
70 fb *neferclient.Feedback
71 rend *gpu.Renderer
72 mod gpu.Modifier
73
74 haveAcquire bool
75 frame gpu.Frame
76 watched [gpu.SlotCount]int // watched eventfds, -1 when none
77
78 runners []runner
79 mods []*module.Module // the modules of runners, in config order
80 runCtx context.Context
81 cancelRun context.CancelFunc
82 lay *layout.Layout
83 modCh chan struct{}
84 cfgPath string
85 cfgCh chan config.Config
86
87 theme theme.Theme
88 themeCh chan struct{}
89 themeCancel context.CancelFunc
90 pal [16][3]uint8
91 env []string // theme colors for the scripts
92
93 // settling is true between a scale or size change and the moment both the
94 // new scale and the new logical width have arrived. They come as separate
95 // events, in either order; building a renderer from half of them presents a
96 // buffer of the wrong size, and some compositors keep that size.
97 settling bool
98 settleC <-chan time.Time
99
100 // fresh is true from the moment a layer surface is created until a renderer
101 // exists for it. A new surface reports scale 1 until the compositor says
102 // otherwise, so its first geometry is not trusted until the events settle.
103 fresh bool
104 // asked is the last (height, scale) a surface was recreated for. Asking for
105 // the same pair twice means the compositor will not give that height, and
106 // the bar takes what it gets instead of looping.
107 askedH int32
108 askedScale float64
109
110 // mappedW is the logical width the current layer surface was first drawn at.
111 // It resets to 0 when the surface is recreated.
112 mappedW int32
113 Stats Stats
114 fatal error
115 closed bool
116}
117
118// New builds a bar from a validated config. cfgPath is the file the config came
119// from: relative theme paths are read from its directory, and the bar reloads
120// it when it changes (empty: no reload). display names the Wayland socket;
121// empty uses $WAYLAND_DISPLAY.
122func New(cfg config.Config, cfgPath string, log *slog.Logger, display string) (*Bar, error) {
123 b := &Bar{cfg: cfg, cfgPath: cfgPath, log: log, display: display, modCh: make(chan struct{}, 1), cfgCh: make(chan config.Config, 1),
124 themeCh: make(chan struct{}, 1)}
125 var err error
126 if err = b.loadTheme(); err != nil {
127 return nil, err
128 }
129 if b.fonts, err = findFonts(cfg.Bar.Font, log); err != nil {
130 return nil, err
131 }
132 for i := range b.watched {
133 b.watched[i] = -1
134 }
135 return b, nil
136}
137
138// barColors picks the bar's own colors: the settings win, otherwise the theme
139// foreground, and a background one step toward the foreground so the bar
140// stands out from a terminal window of the same theme.
141func barColors(cfg config.Config, th theme.Theme) (fg, bg [3]uint8) {
142 fg, bg = th.Foreground, theme.Mix(th.Background, th.Foreground, 0.07)
143 if c, err := config.ParseColor(cfg.Bar.Foreground); err == nil && cfg.Bar.Foreground != "" {
144 fg = c
145 }
146 if c, err := config.ParseColor(cfg.Bar.Background); err == nil && cfg.Bar.Background != "" {
147 bg = c
148 }
149 return fg, bg
150}
151
152// loadTheme resolves bar.theme and applies it. When the theme cannot be read
153// the built-in colors are used, and the problem is logged.
154func (b *Bar) loadTheme() error {
155 base := filepath.Dir(b.cfgPath)
156 if b.cfgPath == "" {
157 base = "."
158 }
159 th, err := theme.Resolve(b.cfg.Bar.Theme, base, theme.SystemEnv())
160 if err != nil {
161 b.log.Warn("using the built-in colors: "+err.Error(), "theme", b.cfg.Bar.Theme)
162 } else {
163 b.log.Info("theme", "source", th.Source)
164 }
165 b.setTheme(th)
166 return nil
167}
168
169// setTheme makes th the colors of the bar and of its scripts.
170func (b *Bar) setTheme(th theme.Theme) {
171 b.theme = th
172 b.fg, b.bg = barColors(b.cfg, th)
173 b.pal = th.Palette
174 b.env = th.EnvVars(b.bg, b.fg, b.cfg.Bar.Accent)
175 if b.lay != nil {
176 b.lay.SetColors(b.fg, b.bg, b.pal)
177 }
178 b.dirty = true
179 b.watchTheme()
180}
181
182// watchTheme watches every file the theme was read from, so changing the
183// terminal's theme changes the bar.
184func (b *Bar) watchTheme() {
185 if b.themeCancel != nil {
186 b.themeCancel()
187 }
188 if b.runCtx == nil {
189 return // Run has not started; it calls this again
190 }
191 ctx, cancel := context.WithCancel(b.runCtx)
192 b.themeCancel = cancel
193 for _, f := range b.theme.Files {
194 go func() {
195 if err := fswatch.Watch(ctx, f, 150*time.Millisecond, b.themeCh); err != nil {
196 b.log.Warn("cannot watch a theme file; edits to it need a restart", "file", f, "err", err)
197 }
198 }()
199 }
200}
201
202// reloadTheme re-reads the theme after one of its files changed.
203func (b *Bar) reloadTheme() {
204 base := filepath.Dir(b.cfgPath)
205 if b.cfgPath == "" {
206 base = "."
207 }
208 th, err := theme.Resolve(b.cfg.Bar.Theme, base, theme.SystemEnv())
209 if err != nil {
210 b.log.Warn("theme reload failed; keeping the current colors", "err", err)
211 return
212 }
213 if th.Equal(b.theme) {
214 // Same colors, but the files that make them up may have changed.
215 if !slices.Equal(th.Files, b.theme.Files) {
216 b.theme.Files = th.Files
217 b.watchTheme()
218 }
219 return
220 }
221 b.log.Info("theme changed", "source", th.Source)
222 b.setTheme(th)
223 b.syncModules(b.cfg.Module) // scripts get the new colors in their environment
224}
225
226// runner is one started module and how to stop it.
227type runner struct {
228 cfg config.Module
229 env []string
230 m *module.Module
231 cancel context.CancelFunc
232}
233
234var zones = map[string]module.Zone{"left": module.Left, "center": module.Center, "right": module.Right}
235
236// syncModules makes the running modules match want. A module with the same
237// name, zone and command keeps running; every other one is stopped, and a new
238// or changed one is started.
239func (b *Bar) syncModules(want []config.Module) {
240 old := make(map[string]runner, len(b.runners))
241 for _, r := range b.runners {
242 old[r.cfg.Name] = r
243 }
244 next := make([]runner, 0, len(want))
245 for _, w := range want {
246 if r, ok := old[w.Name]; ok {
247 delete(old, w.Name)
248 if r.cfg == w && slices.Equal(r.env, b.env) {
249 next = append(next, r)
250 continue
251 }
252 r.cancel()
253 }
254 ctx, cancel := context.WithCancel(b.runCtx)
255 m := module.New(w.Name, zones[w.Zone], w.Exec, b.modCh, b.log)
256 m.Env = b.env
257 m.Start(ctx)
258 next = append(next, runner{cfg: w, env: b.env, m: m, cancel: cancel})
259 }
260 for _, r := range old {
261 r.cancel()
262 }
263 b.runners = next
264 b.mods = b.mods[:0]
265 for _, r := range next {
266 b.mods = append(b.mods, r.m)
267 }
268 if b.lay != nil {
269 b.lay.SetModules(b.mods)
270 }
271}
272
273// applyConfig switches to next. Nothing changes if it cannot be applied.
274func (b *Bar) applyConfig(next config.Config) {
275 old := b.cfg
276 var err error
277 fontChanged := next.Bar.Font != old.Bar.Font
278 fonts := b.fonts
279 if fontChanged {
280 if fonts, err = findFonts(next.Bar.Font, b.log); err != nil {
281 b.log.Warn("config not applied", "err", err)
282 return
283 }
284 }
285 if next.Bar.Output != old.Bar.Output {
286 b.log.Warn("bar.output changed: restart the bar to move it", "from", old.Bar.Output, "to", next.Bar.Output)
287 next.Bar.Output = old.Bar.Output
288 }
289 b.cfg = next
290 b.fonts = fonts
291 if err = b.loadTheme(); err != nil { // the theme setting or the colors may have changed
292 b.log.Warn("theme not applied", "err", err)
293 }
294 b.syncModules(next.Module)
295 b.dirty = true
296 if fontChanged || next.Bar.Size != old.Bar.Size || next.Bar.Scale != old.Bar.Scale {
297 b.facePx = -1 // reconcile rebuilds the font, and the surface if its height changed
298 b.reconcile()
299 }
300 b.log.Info("config reloaded")
301}
302
303// Run connects to the compositor and serves until ctx ends or the surface is
304// closed. It returns the first fatal error.
305func (b *Bar) Run(ctx context.Context) (err error) {
306 b.conn, err = neferclient.Connect(ctx, b.display)
307 if err != nil {
308 return connectError(b.display, err)
309 }
310 defer func() {
311 err = errors.Join(err, b.shutdown())
312 b.log.Info("stats", "presented", b.Stats.Presented, "rebuilds", b.Stats.Rebuilds,
313 "recreated", b.Stats.Recreated, "no_slot", b.Stats.NoSlot, "dirty", b.Stats.Dirty, "module_wakes", b.Stats.ModuleWake)
314 }()
315 if err = b.createSurface(estimateHeight(b.cfg.Bar.Size * b.cfg.Bar.Scale)); err != nil {
316 return err
317 }
318 runCtx, cancel := context.WithCancel(ctx)
319 defer cancel()
320 b.runCtx, b.cancelRun = runCtx, cancel
321 b.watchTheme()
322 b.syncModules(b.cfg.Module)
323 if b.cfgPath != "" {
324 go func() {
325 if err := config.Watch(runCtx, b.cfgPath, b.cfg, b.cfgCh, b.log); err != nil {
326 b.log.Warn("config reload is off", "err", err)
327 }
328 }()
329 }
330 for !b.closed {
331 select {
332 case <-ctx.Done():
333 return nil
334 case <-b.conn.Wake():
335 if err = b.conn.Dispatch(b); err != nil {
336 return fmt.Errorf("dispatch: %w", err)
337 }
338 case <-b.modCh:
339 b.Stats.ModuleWake++
340 case next := <-b.cfgCh:
341 b.applyConfig(next)
342 case <-b.themeCh:
343 b.reloadTheme()
344 case <-b.settleC:
345 b.settleC, b.settling = nil, false
346 b.reconcile()
347 }
348 if b.fatal != nil {
349 return b.fatal
350 }
351 if err = b.step(); err != nil {
352 return err
353 }
354 }
355 return nil
356}
357
358// findFonts resolves the four font files of a family and warns when
359// fontconfig had to substitute another family.
360func findFonts(family string, log *slog.Logger) (glyph.Paths, error) {
361 p, err := glyph.FindFonts(family)
362 if err != nil {
363 return p, err
364 }
365 if got := glyph.FamilyOf(family); got != "" && !strings.EqualFold(got, family) && !strings.HasPrefix(strings.ToLower(family), strings.ToLower(got)) {
366 log.Warn("font not found, fontconfig substituted another family: icons may be missing", "wanted", family, "got", got, "file", p[0])
367 }
368 return p, nil
369}
370
371// socketPath mirrors how neferclient resolves the socket, for error messages.
372func socketPath(display string) string {
373 if display == "" {
374 display = os.Getenv("WAYLAND_DISPLAY")
375 }
376 if display == "" {
377 display = "wayland-0"
378 }
379 if filepath.IsAbs(display) {
380 return display
381 }
382 return filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), display)
383}
384
385// connectError turns a failed dial into one actionable sentence.
386func connectError(display string, err error) error {
387 path := socketPath(display)
388 switch {
389 case errors.Is(err, os.ErrNotExist):
390 return fmt.Errorf("no Wayland socket at %s: is the compositor running? Set WAYLAND_DISPLAY, NEFERBAR_DISPLAY or -display", path)
391 case errors.Is(err, syscall.ECONNREFUSED):
392 return fmt.Errorf("the Wayland socket %s exists but refuses connections: stale socket or compositor not ready", path)
393 case errors.Is(err, os.ErrPermission):
394 return fmt.Errorf("not allowed to open the Wayland socket %s", path)
395 }
396 return err
397}
398
399// estimateHeight is the first request, before the real cell height is known.
400func estimateHeight(px float64) int32 { return int32(math.Ceil(px * 1.5)) }
401
402func (b *Bar) createSurface(h int32) error {
403 surf, err := b.conn.NewLayerSurface(neferclient.LayerConfig{
404 Output: b.cfg.Bar.Output,
405 Namespace: "neferbar",
406 Level: neferclient.LayerTop,
407 Anchors: neferclient.AnchorTop | neferclient.AnchorLeft | neferclient.AnchorRight,
408 ExclusiveZone: h,
409 Height: h,
410 InputRects: []neferclient.Rect{}, // read only: click-through
411 })
412 if err != nil {
413 return fmt.Errorf("layer surface: %w", err)
414 }
415 b.surf, b.sid = surf, surf.ID()
416 b.mappedW = 0
417 b.fresh = true
418 b.configured, b.canPresent = false, false
419 return nil
420}
421
422// step reparses changed modules and draws when something is ready.
423func (b *Bar) step() error {
424 if b.lay != nil && b.lay.Update() {
425 b.dirty = true
426 b.Stats.Dirty++
427 }
428 return b.draw()
429}
430
431func (b *Bar) draw() error {
432 if !b.dirty || !b.canPresent || b.rend == nil || b.settling {
433 return nil
434 }
435 ok, err := b.rend.Draw(b.lay.Compose(), b.bg, &b.frame)
436 if err != nil {
437 return fmt.Errorf("draw: %w", err)
438 }
439 if !ok {
440 b.Stats.NoSlot++ // Released retries
441 return nil
442 }
443 if err = b.present(); err != nil {
444 return err
445 }
446 b.dirty, b.canPresent = false, false
447 b.Stats.Presented++
448 return nil
449}
450
451func (b *Bar) present() error {
452 f := &b.frame
453 if f.NewBuffer {
454 img := f.Image
455 buf := neferclient.Buffer{
456 Width: img.Width, Height: img.Height, FourCC: gpu.DRMFormatXRGB8888, Modifier: img.Modifier, PlaneCount: 1,
457 }
458 buf.Planes[0] = neferclient.Plane{FD: img.FD, Offset: img.Offset, Stride: img.Stride}
459 if err := b.surf.ImportBuffer(uint64(f.Slot), &buf); err != nil {
460 return fmt.Errorf("import buffer: %w", err)
461 }
462 if err := b.surf.ImportTimeline(uint64(f.Slot), f.ReleaseTimeline); err != nil {
463 return fmt.Errorf("import release timeline: %w", err)
464 }
465 if err := b.conn.WatchFD(f.ReleaseEventFD, uint64(f.Slot)); err != nil {
466 return fmt.Errorf("watch release fd: %w", err)
467 }
468 b.watched[f.Slot] = f.ReleaseEventFD
469 }
470 if !b.haveAcquire {
471 if err := b.surf.ImportTimeline(acquireTimelineID, b.rend.AcquireTimelineFD()); err != nil {
472 return fmt.Errorf("import acquire timeline: %w", err)
473 }
474 b.haveAcquire = true
475 }
476 err := b.surf.Present(&neferclient.Present{
477 Buffer: uint64(f.Slot),
478 AcquireTimeline: acquireTimelineID,
479 ReleaseTimeline: uint64(f.Slot),
480 AcquirePoint: f.AcquirePoint,
481 ReleasePoint: f.ReleasePoint,
482 Opaque: true,
483 })
484 if err != nil {
485 return fmt.Errorf("present: %w", err)
486 }
487 return nil
488}
489
490// reconcile brings the font, the layer surface height and the renderer in
491// line with the current scale and feedback. It runs on every event that can
492// change them and does nothing when everything already matches.
493func (b *Bar) reconcile() {
494 if !b.configured || b.fb == nil {
495 return
496 }
497 if b.dev == nil {
498 dev, err := gpu.Open(b.fb.MainDevice)
499 if err != nil {
500 b.fail(err)
501 return
502 }
503 node, err := syncobj.Open(filepath.Join("/dev/dri", fmt.Sprintf("renderD%d", dev.RenderMinor)))
504 if err != nil {
505 dev.Close()
506 b.fail(err)
507 return
508 }
509 b.dev, b.node = dev, node
510 }
511 w, h, scale := b.surf.Size()
512 px := b.cfg.Bar.Size * b.cfg.Bar.Scale * scale
513 if b.face == nil || b.facePx != px {
514 face, err := glyph.Load(b.fonts, px)
515 if err != nil {
516 b.fail(err)
517 return
518 }
519 b.dropRenderer()
520 b.face.Close()
521 b.face, b.facePx = face, px
522 }
523 // The bar is one cell high: logical height = ceil(cell / scale).
524 wantH := int32(math.Ceil(float64(b.face.CellH)/scale - 1e-9))
525 if h != wantH && b.askedH == wantH && b.askedScale == scale {
526 // Already asked for exactly this and got something else: the compositor
527 // clamps the height. Use what we have rather than asking forever.
528 b.log.Warn("the compositor gave another bar height than asked; using it", "asked", wantH, "got", h)
529 wantH = h
530 }
531 if h != wantH {
532 b.askedH, b.askedScale = wantH, scale
533 b.log.Info("recreating layer surface", "height", wantH, "scale", scale)
534 b.dropRenderer()
535 if err := b.surf.Close(); err != nil {
536 b.fail(err)
537 return
538 }
539 if err := b.createSurface(wantH); err != nil {
540 b.fail(err)
541 return
542 }
543 b.Stats.Recreated++
544 return
545 }
546 pw, ph, err := b.surf.PhysicalSize()
547 if err != nil {
548 b.fail(err)
549 return
550 }
551 if b.rend != nil && b.rend.W == pw && b.rend.H == ph {
552 return
553 }
554 if b.mappedW != 0 && w != b.mappedW {
555 // The logical width changed (a scale change on the output). NeferWL
556 // places a layer surface from the size it had when it was mapped and
557 // does not re-read it when only the buffer and viewport change, so the
558 // old width would stay: centered in a narrower output, the left part cut
559 // off. A new surface is mapped fresh and placed from its real size.
560 b.log.Info("recreating layer surface: the output width changed", "from", b.mappedW, "to", w)
561 b.dropRenderer()
562 if err := b.surf.Close(); err != nil {
563 b.fail(err)
564 return
565 }
566 if err := b.createSurface(h); err != nil {
567 b.fail(err)
568 return
569 }
570 b.Stats.Recreated++
571 return
572 }
573 b.dropRenderer()
574 if mod, err := b.chooseModifier(); err != nil {
575 b.fail(err)
576 return
577 } else {
578 b.mod = mod
579 }
580 rend, err := gpu.NewRenderer(b.dev, b.node, b.mod, b.face, pw, ph)
581 if err != nil {
582 b.fail(err)
583 return
584 }
585 b.rend, b.mappedW = rend, w
586 b.fresh = false
587 b.Stats.Rebuilds++
588 if b.lay == nil {
589 b.lay = layout.New(rend.Cols(), b.fg, b.bg, b.pal, b.mods)
590 } else {
591 b.lay.Resize(rend.Cols())
592 }
593 b.lay.Update()
594 b.dirty = true
595 b.log.Info("renderer ready", "px", pw, "py", ph, "scale", scale, "cell", fmt.Sprintf("%dx%d", b.face.CellW, b.face.CellH), "cols", rend.Cols())
596}
597
598func (b *Bar) chooseModifier() (gpu.Modifier, error) {
599 supported, err := b.dev.ExportableModifiers()
600 if err != nil {
601 return 0, err
602 }
603 pairs := make([]gpu.FormatPair, len(b.fb.Formats))
604 for i, f := range b.fb.Formats {
605 pairs[i] = gpu.FormatPair{FourCC: f.FourCC, Modifier: f.Modifier}
606 }
607 return gpu.ChooseModifier(pairs, supported)
608}
609
610// dropRenderer retires the renderer and every wl object that refers to its
611// images.
612func (b *Bar) dropRenderer() {
613 if b.rend == nil {
614 return
615 }
616 for i, fd := range b.watched {
617 if fd >= 0 {
618 _ = b.conn.UnwatchFD(fd)
619 b.watched[i] = -1
620 }
621 }
622 if err := b.surf.DestroyImports(); err != nil {
623 b.log.Warn("destroy imports", "err", err)
624 }
625 b.haveAcquire = false
626 b.rend.Close()
627 b.rend = nil
628}
629
630func (b *Bar) fail(err error) {
631 if b.fatal == nil {
632 b.fatal = err
633 }
634}
635
636func (b *Bar) shutdown() error {
637 var errs []error
638 // Stop the scripts first: cancel asks them to end, Wait gives them time to,
639 // so none outlives the bar.
640 if b.cancelRun != nil {
641 b.cancelRun()
642 }
643 for _, r := range b.runners {
644 r.cancel()
645 }
646 for _, r := range b.runners {
647 r.m.Wait(3 * time.Second)
648 }
649 b.dropRenderer()
650 b.face.Close()
651 if b.node != nil {
652 errs = append(errs, b.node.Close())
653 }
654 b.dev.Close()
655 if b.conn != nil {
656 errs = append(errs, b.conn.Close())
657 }
658 return errors.Join(errs...)
659}
660
661// settleDelay is how long the bar waits for the second half of a scale or size
662// change before it rebuilds.
663const settleDelay = 60 * time.Millisecond
664
665// geometryChanged is called when the compositor reports a new scale or size.
666// The first report builds at once, because there is nothing on screen yet.
667// Later ones wait for the dust to settle, and nothing is drawn meanwhile.
668func (b *Bar) geometryChanged() {
669 if b.rend == nil && !b.fresh {
670 b.reconcile()
671 return
672 }
673 b.settling = true
674 b.settleC = time.After(settleDelay)
675}
676
677// Handler methods. Events of a replaced surface are ignored.
678
679// Configure marks the surface presentable and rebuilds as needed.
680func (b *Bar) Configure(id neferclient.SurfaceID, _, _ int32) {
681 if id != b.sid {
682 return
683 }
684 if !b.configured {
685 b.canPresent = true
686 }
687 b.configured = true
688 b.geometryChanged()
689}
690
691// Scale follows the monitor's preferred scale.
692func (b *Bar) Scale(id neferclient.SurfaceID, _ float64) {
693 if id == b.sid {
694 b.geometryChanged()
695 }
696}
697
698// FeedbackDone records the compositor's formats; a changed main device is not
699// followed.
700func (b *Bar) FeedbackDone(id neferclient.SurfaceID) {
701 if id != b.sid {
702 return
703 }
704 fb := b.surf.Feedback()
705 if b.fb != nil && b.fb.MainDevice != fb.MainDevice {
706 b.log.Warn("dmabuf main device changed; restart the bar to follow it")
707 return
708 }
709 if b.fb.Equal(fb) {
710 return
711 }
712 b.fb = fb.Clone()
713 b.dropRenderer() // formats changed: images must be rebuilt
714 b.reconcile()
715}
716
717// Frame allows the next Present.
718func (b *Bar) Frame(id neferclient.SurfaceID) {
719 if id == b.sid {
720 b.canPresent = true
721 }
722}
723
724// Closed ends the loop when the compositor closes the surface.
725func (b *Bar) Closed(id neferclient.SurfaceID) {
726 if id == b.sid {
727 b.closed = true
728 }
729}
730
731// FDReady hands a slot back once the compositor released it.
732func (b *Bar) FDReady(id uint64) {
733 if b.rend == nil {
734 return
735 }
736 if err := b.rend.Released(int(id)); err != nil {
737 b.fail(err)
738 }
739}
740
741// Error logs non-fatal connection errors.
742func (b *Bar) Error(err error) { b.log.Warn("wayland", "err", err) }
743
744// Unused events.
745func (b *Bar) OutputAdded(*neferclient.Output) {}
746func (b *Bar) OutputRemoved(uint32) {}
747func (b *Bar) Locked() {}
748func (b *Bar) LockFinished() {}
749func (b *Bar) Pointer(*neferclient.PointerEvent) {}
750func (b *Bar) Key(*neferclient.KeyEvent) {}
751func (b *Bar) KeyboardFocus(neferclient.SurfaceID, bool) {}
752func (b *Bar) SecretChanged(int) {}