fix(examples): handle output removal, per-screen cursor and focus marker in lock
- commit
- 63e3cbe3a263eb2ca1a7d4a6ff69e13047f8ffb6
- author
- brice · · 2026-10-01 19:23 UTC
- parent
- 1988d1e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +96 -16
M examples/lock/main.go +96 -16
| @@ -12,8 +12,9 @@ | |||
| 12 | 12 | // SecretBuffer.Len(). The program never converts the secret to a string and | |
| 13 | 13 | // never prints it; the buffer is wiped on unlock and on exit. | |
| 14 | 14 | // | |
| 15 | - | // The program prints "locked" and "unlocked" on standard output as progress | |
| 16 | - | // markers (the headless test waits for them). | |
| 15 | + | // The program prints "locked", "focused" (the keyboard reached a lock surface) | |
| 16 | + | // and "unlocked" on standard output as progress markers; the headless test | |
| 17 | + | // waits for them. It never prints anything typed. | |
| 17 | 18 | // | |
| 18 | 19 | // All logic runs on the owner goroutine, inside or right after Conn.Dispatch. | |
| 19 | 20 | // The lock needs one surface per output; each gets its own Renderer. | |
| @@ -25,6 +26,7 @@ import ( | |||
| 25 | 26 | "fmt" | |
| 26 | 27 | "os" | |
| 27 | 28 | "os/signal" | |
| 29 | + | "syscall" | |
| 28 | 30 | "time" | |
| 29 | 31 | ||
| 30 | 32 | "github.com/bnema/neferclient" | |
| @@ -33,6 +35,10 @@ import ( | |||
| 33 | 35 | ||
| 34 | 36 | const keysymReturn, keysymKPEnter = 0xff0d, 0xff8d // xkb keysyms | |
| 35 | 37 | ||
| 38 | + | // guiMods is the set of modifiers NeferGUI knows. neferclient and NeferGUI use | |
| 39 | + | // the same bit order for them, so the plain bits can be copied. | |
| 40 | + | const guiMods = neferclient.ModShift | neferclient.ModCtrl | neferclient.ModAlt | neferclient.ModSuper | |
| 41 | + | ||
| 36 | 42 | // model is what the view reads. The secret stays in its buffer; the view only | |
| 37 | 43 | // asks for its length. | |
| 38 | 44 | type model struct{ secret *neferclient.SecretBuffer } | |
| @@ -47,6 +53,7 @@ func view(f *nefergui.Frame, m *model) { | |||
| 47 | 53 | ||
| 48 | 54 | // screen is the lock surface of one output and its renderer. | |
| 49 | 55 | type screen struct { | |
| 56 | + | output uint32 // wl_output registry name (Output.Global) | |
| 50 | 57 | conn *neferclient.Conn | |
| 51 | 58 | model *model | |
| 52 | 59 | surf *neferclient.Surface | |
| @@ -55,6 +62,7 @@ type screen struct { | |||
| 55 | 62 | damage []neferclient.Rect | |
| 56 | 63 | ||
| 57 | 64 | configured, canPresent, haveAcquire bool | |
| 65 | + | live map[uint64]int // buffer → release eventfd still watched | |
| 58 | 66 | } | |
| 59 | 67 | ||
| 60 | 68 | type app struct { | |
| @@ -68,12 +76,13 @@ type app struct { | |||
| 68 | 76 | screens map[neferclient.SurfaceID]*screen | |
| 69 | 77 | ||
| 70 | 78 | cursor neferclient.CursorShape | |
| 79 | + | hover neferclient.SurfaceID // surface under the pointer, 0 outside | |
| 71 | 80 | locked, unlocked bool | |
| 72 | 81 | err error | |
| 73 | 82 | } | |
| 74 | 83 | ||
| 75 | 84 | func main() { | |
| 76 | - | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) | |
| 85 | + | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | |
| 77 | 86 | defer stop() | |
| 78 | 87 | if err := run(ctx); err != nil { | |
| 79 | 88 | fmt.Fprintln(os.Stderr, "lock:", err) | |
| @@ -89,12 +98,15 @@ func run(ctx context.Context) (err error) { | |||
| 89 | 98 | a := &app{conn: conn, screens: map[neferclient.SurfaceID]*screen{}, cursor: neferclient.CursorDefault} | |
| 90 | 99 | a.secret = neferclient.NewSecretBuffer(256) | |
| 91 | 100 | a.model.secret = a.secret | |
| 101 | + | // Shutdown order: wipe the secret, close the connection (this drops its | |
| 102 | + | // descriptor watches), then free the renderers and their GPU resources, | |
| 103 | + | // including the release eventfds. The same order as the layer example. | |
| 92 | 104 | defer func() { | |
| 93 | 105 | a.secret.Wipe() | |
| 106 | + | err = errors.Join(err, conn.Close()) | |
| 94 | 107 | for _, s := range a.screens { | |
| 95 | 108 | err = errors.Join(err, s.close()) | |
| 96 | 109 | } | |
| 97 | - | err = errors.Join(err, conn.Close()) | |
| 98 | 110 | }() | |
| 99 | 111 | ||
| 100 | 112 | // The compositor may announce its first output just after Connect. | |
| @@ -105,7 +117,7 @@ func run(ctx context.Context) (err error) { | |||
| 105 | 117 | return fmt.Errorf("dispatch: %w", err) | |
| 106 | 118 | } | |
| 107 | 119 | case <-ctx.Done(): | |
| 108 | - | return ctx.Err() | |
| 120 | + | return nil // interrupt or SIGTERM before the lock: nothing to undo | |
| 109 | 121 | } | |
| 110 | 122 | } | |
| 111 | 123 | if a.lock, err = conn.Lock(); err != nil { | |
| @@ -125,7 +137,10 @@ func run(ctx context.Context) (err error) { | |||
| 125 | 137 | for !a.unlocked { | |
| 126 | 138 | select { | |
| 127 | 139 | case <-ctx.Done(): | |
| 128 | - | return ctx.Err() // the session stays locked: only Unlock ends it | |
| 140 | + | // Interrupt or SIGTERM: a clean exit. The session stays locked, | |
| 141 | + | // because only Lock.Unlock ends a lock; the compositor decides | |
| 142 | + | // what happens to a lock whose client is gone. | |
| 143 | + | return nil | |
| 129 | 144 | case <-conn.Wake(): | |
| 130 | 145 | if err = conn.Dispatch(a); err != nil { | |
| 131 | 146 | return fmt.Errorf("dispatch: %w", err) | |
| @@ -144,8 +159,9 @@ func run(ctx context.Context) (err error) { | |||
| 144 | 159 | if err != nil { | |
| 145 | 160 | return err | |
| 146 | 161 | } | |
| 147 | - | if shown { | |
| 148 | - | // The cursor the hovered element asked for → Seat.SetCursor. | |
| 162 | + | // The cursor the hovered element asked for → Seat.SetCursor, only | |
| 163 | + | // from the screen under the pointer. | |
| 164 | + | if shown && s.surf.ID() == a.hover { | |
| 149 | 165 | a.cursor = cursorShape(s.out.Cursor) | |
| 150 | 166 | if err = a.seat.SetCursor(a.cursor); err != nil { | |
| 151 | 167 | return fmt.Errorf("cursor: %w", err) | |
| @@ -166,7 +182,7 @@ func (a *app) addScreen(output uint32) error { | |||
| 166 | 182 | if err != nil { | |
| 167 | 183 | return fmt.Errorf("lock surface: %w", err) | |
| 168 | 184 | } | |
| 169 | - | a.screens[surf.ID()] = &screen{conn: a.conn, model: &a.model, surf: surf} | |
| 185 | + | a.screens[surf.ID()] = &screen{output: output, conn: a.conn, model: &a.model, surf: surf, live: map[uint64]int{}} | |
| 170 | 186 | return nil | |
| 171 | 187 | } | |
| 172 | 188 | ||
| @@ -188,6 +204,23 @@ func (a *app) OutputAdded(out *neferclient.Output) { | |||
| 188 | 204 | } | |
| 189 | 205 | } | |
| 190 | 206 | ||
| 207 | + | // OutputRemoved: stop watching the screen's release fds, free its renderer and | |
| 208 | + | // surface, and forget it. | |
| 209 | + | func (a *app) OutputRemoved(global uint32) { | |
| 210 | + | for id, s := range a.screens { | |
| 211 | + | if s.output != global { | |
| 212 | + | continue | |
| 213 | + | } | |
| 214 | + | if a.hover == id { | |
| 215 | + | a.hover = 0 | |
| 216 | + | } | |
| 217 | + | delete(a.screens, id) | |
| 218 | + | if err := s.release(); err != nil { | |
| 219 | + | a.fail(fmt.Errorf("output %d removed: %w", global, err)) | |
| 220 | + | } | |
| 221 | + | } | |
| 222 | + | } | |
| 223 | + | ||
| 191 | 224 | func (a *app) Locked() { | |
| 192 | 225 | a.locked = true | |
| 193 | 226 | fmt.Println("locked") | |
| @@ -198,17 +231,30 @@ func (a *app) LockFinished() { | |||
| 198 | 231 | } | |
| 199 | 232 | ||
| 200 | 233 | // FeedbackDone: dmabuf feedback → nefergui.RendererConfig. | |
| 234 | + | // | |
| 235 | + | // Only the first complete feedback is used. A later one (the compositor | |
| 236 | + | // changing its preferred device or formats) is ignored: this demo assumes a | |
| 237 | + | // single GPU, and rebuilding the renderers is application policy that would | |
| 238 | + | // obscure the glue. A real application would create a new Renderer here and | |
| 239 | + | // re-import its buffers. | |
| 201 | 240 | func (a *app) FeedbackDone(id neferclient.SurfaceID) { | |
| 202 | 241 | if s := a.screens[id]; s != nil { | |
| 203 | - | a.fail(s.setup()) | |
| 242 | + | if err := s.setup(); err != nil { | |
| 243 | + | a.fail(err) | |
| 244 | + | } | |
| 204 | 245 | } | |
| 205 | 246 | } | |
| 206 | 247 | ||
| 207 | 248 | func (a *app) Configure(id neferclient.SurfaceID, _, _ int32) { | |
| 208 | 249 | if s := a.screens[id]; s != nil { | |
| 209 | - | s.configured, s.canPresent = true, true | |
| 250 | + | if !s.configured { // later configures must not lift the frame-callback gate | |
| 251 | + | s.canPresent = true | |
| 252 | + | } | |
| 253 | + | s.configured = true | |
| 210 | 254 | s.resize() | |
| 211 | - | a.fail(s.setup()) | |
| 255 | + | if err := s.setup(); err != nil { | |
| 256 | + | a.fail(err) | |
| 257 | + | } | |
| 212 | 258 | } | |
| 213 | 259 | } | |
| 214 | 260 | ||
| @@ -238,15 +284,26 @@ func (a *app) FDReady(id uint64) { | |||
| 238 | 284 | ||
| 239 | 285 | // Pointer: neferclient events → nefergui.Input, field by field. | |
| 240 | 286 | func (a *app) Pointer(ev *neferclient.PointerEvent) { | |
| 287 | + | // Track the screen under the pointer first: the cursor shape is applied | |
| 288 | + | // only from it. | |
| 289 | + | switch ev.Kind { | |
| 290 | + | case neferclient.PointerEnter: | |
| 291 | + | a.hover = ev.Surface | |
| 292 | + | // The shape only applies after an enter: send the last one again. | |
| 293 | + | if err := a.seat.SetCursor(a.cursor); err != nil { | |
| 294 | + | a.fail(fmt.Errorf("cursor: %w", err)) | |
| 295 | + | } | |
| 296 | + | case neferclient.PointerLeave: | |
| 297 | + | if a.hover == ev.Surface { | |
| 298 | + | a.hover = 0 | |
| 299 | + | } | |
| 300 | + | } | |
| 241 | 301 | s := a.screens[ev.Surface] | |
| 242 | 302 | if s == nil || s.r == nil { | |
| 243 | 303 | return | |
| 244 | 304 | } | |
| 245 | 305 | in := nefergui.Input{X: ev.X, Y: ev.Y, Kind: nefergui.InputPointerMotion} | |
| 246 | 306 | switch ev.Kind { | |
| 247 | - | case neferclient.PointerEnter: | |
| 248 | - | // The shape only applies after an enter: send the last one again. | |
| 249 | - | a.fail(a.seat.SetCursor(a.cursor)) | |
| 250 | 307 | case neferclient.PointerLeave: | |
| 251 | 308 | in.Kind = nefergui.InputPointerLeave | |
| 252 | 309 | case neferclient.PointerButton: | |
| @@ -274,12 +331,15 @@ func (a *app) Key(ev *neferclient.KeyEvent) { | |||
| 274 | 331 | if s := a.screens[ev.Surface]; s != nil && s.r != nil { | |
| 275 | 332 | s.r.Input(&nefergui.Input{ | |
| 276 | 333 | Kind: nefergui.InputKey, Keysym: ev.Keysym, Pressed: ev.Pressed, Repeat: ev.Repeat, | |
| 277 | - | Modifiers: nefergui.Modifiers(ev.Modifiers & 0x0f), // shift, ctrl, alt, super | |
| 334 | + | Modifiers: nefergui.Modifiers(ev.Modifiers & guiMods), | |
| 278 | 335 | }) | |
| 279 | 336 | } | |
| 280 | 337 | } | |
| 281 | 338 | ||
| 282 | 339 | func (a *app) KeyboardFocus(id neferclient.SurfaceID, focused bool) { | |
| 340 | + | if focused { | |
| 341 | + | fmt.Println("focused") | |
| 342 | + | } | |
| 283 | 343 | if s := a.screens[id]; s != nil && s.r != nil { | |
| 284 | 344 | kind := nefergui.InputFocusOut | |
| 285 | 345 | if focused { | |
| @@ -376,9 +436,14 @@ func (s *screen) present() error { | |||
| 376 | 436 | if err := s.conn.UnwatchFD(rt.ReleaseFD); err != nil { | |
| 377 | 437 | return fmt.Errorf("unwatch retired buffer %d: %w", rt.Buffer, err) | |
| 378 | 438 | } | |
| 439 | + | delete(s.live, rt.Buffer) | |
| 379 | 440 | if err := s.surf.DestroyBuffer(rt.Buffer); err != nil { | |
| 380 | 441 | return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err) | |
| 381 | 442 | } | |
| 443 | + | // The release timeline of a buffer is imported under the buffer's id. | |
| 444 | + | if err := s.surf.DestroyTimeline(rt.Buffer); err != nil { | |
| 445 | + | return fmt.Errorf("destroy retired timeline %d: %w", rt.Buffer, err) | |
| 446 | + | } | |
| 382 | 447 | } | |
| 383 | 448 | if out.NewBuffer { | |
| 384 | 449 | buf := neferclient.Buffer{ | |
| @@ -396,6 +461,7 @@ func (s *screen) present() error { | |||
| 396 | 461 | if err := s.conn.WatchFD(out.ReleaseFD, s.watchID(out.Buffer)); err != nil { | |
| 397 | 462 | return fmt.Errorf("watch release fd: %w", err) | |
| 398 | 463 | } | |
| 464 | + | s.live[out.Buffer] = out.ReleaseFD | |
| 399 | 465 | } | |
| 400 | 466 | if out.NewTimelines { | |
| 401 | 467 | if !s.haveAcquire { // the acquire timeline is shared by every buffer | |
| @@ -428,6 +494,20 @@ func (s *screen) present() error { | |||
| 428 | 494 | return nil | |
| 429 | 495 | } | |
| 430 | 496 | ||
| 497 | + | // release stops watching the live release eventfds, then frees the renderer and | |
| 498 | + | // the surface (an output went away). The release eventfds belong to the | |
| 499 | + | // renderer, so they are unwatched before it closes. | |
| 500 | + | func (s *screen) release() error { | |
| 501 | + | var errs []error | |
| 502 | + | for buffer, fd := range s.live { | |
| 503 | + | if err := s.conn.UnwatchFD(fd); err != nil { | |
| 504 | + | errs = append(errs, fmt.Errorf("unwatch buffer %d: %w", buffer, err)) | |
| 505 | + | } | |
| 506 | + | } | |
| 507 | + | clear(s.live) | |
| 508 | + | return errors.Join(append(errs, s.close())...) | |
| 509 | + | } | |
| 510 | + | ||
| 431 | 511 | // close frees the renderer and the surface. The session lock itself is only | |
| 432 | 512 | // ended by Lock.Unlock. | |
| 433 | 513 | func (s *screen) close() error { | |
| @@ -12,8 +12,9 @@ | |||||
| 12 | // SecretBuffer.Len(). The program never converts the secret to a string and | 12 | // SecretBuffer.Len(). The program never converts the secret to a string and | ||
| 13 | // never prints it; the buffer is wiped on unlock and on exit. | 13 | // never prints it; the buffer is wiped on unlock and on exit. | ||
| 14 | // | 14 | // | ||
| 15 | - | // The program prints "locked" and "unlocked" on standard output as progress | 15 | + | // The program prints "locked", "focused" (the keyboard reached a lock surface) |
| 16 | - | // markers (the headless test waits for them). | 16 | + | // and "unlocked" on standard output as progress markers; the headless test |
| 17 | + | // waits for them. It never prints anything typed. | |||
| 17 | // | 18 | // | ||
| 18 | // All logic runs on the owner goroutine, inside or right after Conn.Dispatch. | 19 | // All logic runs on the owner goroutine, inside or right after Conn.Dispatch. | ||
| 19 | // The lock needs one surface per output; each gets its own Renderer. | 20 | // The lock needs one surface per output; each gets its own Renderer. | ||
| @@ -25,6 +26,7 @@ import ( | |||||
| 25 | "fmt" | 26 | "fmt" | ||
| 26 | "os" | 27 | "os" | ||
| 27 | "os/signal" | 28 | "os/signal" | ||
| 29 | + | "syscall" | |||
| 28 | "time" | 30 | "time" | ||
| 29 | 31 | ||||
| 30 | "github.com/bnema/neferclient" | 32 | "github.com/bnema/neferclient" | ||
| @@ -33,6 +35,10 @@ import ( | |||||
| 33 | 35 | ||||
| 34 | const keysymReturn, keysymKPEnter = 0xff0d, 0xff8d // xkb keysyms | 36 | const keysymReturn, keysymKPEnter = 0xff0d, 0xff8d // xkb keysyms | ||
| 35 | 37 | ||||
| 38 | + | // guiMods is the set of modifiers NeferGUI knows. neferclient and NeferGUI use | |||
| 39 | + | // the same bit order for them, so the plain bits can be copied. | |||
| 40 | + | const guiMods = neferclient.ModShift | neferclient.ModCtrl | neferclient.ModAlt | neferclient.ModSuper | |||
| 41 | + | ||||
| 36 | // model is what the view reads. The secret stays in its buffer; the view only | 42 | // model is what the view reads. The secret stays in its buffer; the view only | ||
| 37 | // asks for its length. | 43 | // asks for its length. | ||
| 38 | type model struct{ secret *neferclient.SecretBuffer } | 44 | type model struct{ secret *neferclient.SecretBuffer } | ||
| @@ -47,6 +53,7 @@ func view(f *nefergui.Frame, m *model) { | |||||
| 47 | 53 | ||||
| 48 | // screen is the lock surface of one output and its renderer. | 54 | // screen is the lock surface of one output and its renderer. | ||
| 49 | type screen struct { | 55 | type screen struct { | ||
| 56 | + | output uint32 // wl_output registry name (Output.Global) | |||
| 50 | conn *neferclient.Conn | 57 | conn *neferclient.Conn | ||
| 51 | model *model | 58 | model *model | ||
| 52 | surf *neferclient.Surface | 59 | surf *neferclient.Surface | ||
| @@ -55,6 +62,7 @@ type screen struct { | |||||
| 55 | damage []neferclient.Rect | 62 | damage []neferclient.Rect | ||
| 56 | 63 | ||||
| 57 | configured, canPresent, haveAcquire bool | 64 | configured, canPresent, haveAcquire bool | ||
| 65 | + | live map[uint64]int // buffer → release eventfd still watched | |||
| 58 | } | 66 | } | ||
| 59 | 67 | ||||
| 60 | type app struct { | 68 | type app struct { | ||
| @@ -68,12 +76,13 @@ type app struct { | |||||
| 68 | screens map[neferclient.SurfaceID]*screen | 76 | screens map[neferclient.SurfaceID]*screen | ||
| 69 | 77 | ||||
| 70 | cursor neferclient.CursorShape | 78 | cursor neferclient.CursorShape | ||
| 79 | + | hover neferclient.SurfaceID // surface under the pointer, 0 outside | |||
| 71 | locked, unlocked bool | 80 | locked, unlocked bool | ||
| 72 | err error | 81 | err error | ||
| 73 | } | 82 | } | ||
| 74 | 83 | ||||
| 75 | func main() { | 84 | func main() { | ||
| 76 | - | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) | 85 | + | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) |
| 77 | defer stop() | 86 | defer stop() | ||
| 78 | if err := run(ctx); err != nil { | 87 | if err := run(ctx); err != nil { | ||
| 79 | fmt.Fprintln(os.Stderr, "lock:", err) | 88 | fmt.Fprintln(os.Stderr, "lock:", err) | ||
| @@ -89,12 +98,15 @@ func run(ctx context.Context) (err error) { | |||||
| 89 | a := &app{conn: conn, screens: map[neferclient.SurfaceID]*screen{}, cursor: neferclient.CursorDefault} | 98 | a := &app{conn: conn, screens: map[neferclient.SurfaceID]*screen{}, cursor: neferclient.CursorDefault} | ||
| 90 | a.secret = neferclient.NewSecretBuffer(256) | 99 | a.secret = neferclient.NewSecretBuffer(256) | ||
| 91 | a.model.secret = a.secret | 100 | a.model.secret = a.secret | ||
| 101 | + | // Shutdown order: wipe the secret, close the connection (this drops its | |||
| 102 | + | // descriptor watches), then free the renderers and their GPU resources, | |||
| 103 | + | // including the release eventfds. The same order as the layer example. | |||
| 92 | defer func() { | 104 | defer func() { | ||
| 93 | a.secret.Wipe() | 105 | a.secret.Wipe() | ||
| 106 | + | err = errors.Join(err, conn.Close()) | |||
| 94 | for _, s := range a.screens { | 107 | for _, s := range a.screens { | ||
| 95 | err = errors.Join(err, s.close()) | 108 | err = errors.Join(err, s.close()) | ||
| 96 | } | 109 | } | ||
| 97 | - | err = errors.Join(err, conn.Close()) | |||
| 98 | }() | 110 | }() | ||
| 99 | 111 | ||||
| 100 | // The compositor may announce its first output just after Connect. | 112 | // The compositor may announce its first output just after Connect. | ||
| @@ -105,7 +117,7 @@ func run(ctx context.Context) (err error) { | |||||
| 105 | return fmt.Errorf("dispatch: %w", err) | 117 | return fmt.Errorf("dispatch: %w", err) | ||
| 106 | } | 118 | } | ||
| 107 | case <-ctx.Done(): | 119 | case <-ctx.Done(): | ||
| 108 | - | return ctx.Err() | 120 | + | return nil // interrupt or SIGTERM before the lock: nothing to undo |
| 109 | } | 121 | } | ||
| 110 | } | 122 | } | ||
| 111 | if a.lock, err = conn.Lock(); err != nil { | 123 | if a.lock, err = conn.Lock(); err != nil { | ||
| @@ -125,7 +137,10 @@ func run(ctx context.Context) (err error) { | |||||
| 125 | for !a.unlocked { | 137 | for !a.unlocked { | ||
| 126 | select { | 138 | select { | ||
| 127 | case <-ctx.Done(): | 139 | case <-ctx.Done(): | ||
| 128 | - | return ctx.Err() // the session stays locked: only Unlock ends it | 140 | + | // Interrupt or SIGTERM: a clean exit. The session stays locked, |
| 141 | + | // because only Lock.Unlock ends a lock; the compositor decides | |||
| 142 | + | // what happens to a lock whose client is gone. | |||
| 143 | + | return nil | |||
| 129 | case <-conn.Wake(): | 144 | case <-conn.Wake(): | ||
| 130 | if err = conn.Dispatch(a); err != nil { | 145 | if err = conn.Dispatch(a); err != nil { | ||
| 131 | return fmt.Errorf("dispatch: %w", err) | 146 | return fmt.Errorf("dispatch: %w", err) | ||
| @@ -144,8 +159,9 @@ func run(ctx context.Context) (err error) { | |||||
| 144 | if err != nil { | 159 | if err != nil { | ||
| 145 | return err | 160 | return err | ||
| 146 | } | 161 | } | ||
| 147 | - | if shown { | 162 | + | // The cursor the hovered element asked for → Seat.SetCursor, only |
| 148 | - | // The cursor the hovered element asked for → Seat.SetCursor. | 163 | + | // from the screen under the pointer. |
| 164 | + | if shown && s.surf.ID() == a.hover { | |||
| 149 | a.cursor = cursorShape(s.out.Cursor) | 165 | a.cursor = cursorShape(s.out.Cursor) | ||
| 150 | if err = a.seat.SetCursor(a.cursor); err != nil { | 166 | if err = a.seat.SetCursor(a.cursor); err != nil { | ||
| 151 | return fmt.Errorf("cursor: %w", err) | 167 | return fmt.Errorf("cursor: %w", err) | ||
| @@ -166,7 +182,7 @@ func (a *app) addScreen(output uint32) error { | |||||
| 166 | if err != nil { | 182 | if err != nil { | ||
| 167 | return fmt.Errorf("lock surface: %w", err) | 183 | return fmt.Errorf("lock surface: %w", err) | ||
| 168 | } | 184 | } | ||
| 169 | - | a.screens[surf.ID()] = &screen{conn: a.conn, model: &a.model, surf: surf} | 185 | + | a.screens[surf.ID()] = &screen{output: output, conn: a.conn, model: &a.model, surf: surf, live: map[uint64]int{}} |
| 170 | return nil | 186 | return nil | ||
| 171 | } | 187 | } | ||
| 172 | 188 | ||||
| @@ -188,6 +204,23 @@ func (a *app) OutputAdded(out *neferclient.Output) { | |||||
| 188 | } | 204 | } | ||
| 189 | } | 205 | } | ||
| 190 | 206 | ||||
| 207 | + | // OutputRemoved: stop watching the screen's release fds, free its renderer and | |||
| 208 | + | // surface, and forget it. | |||
| 209 | + | func (a *app) OutputRemoved(global uint32) { | |||
| 210 | + | for id, s := range a.screens { | |||
| 211 | + | if s.output != global { | |||
| 212 | + | continue | |||
| 213 | + | } | |||
| 214 | + | if a.hover == id { | |||
| 215 | + | a.hover = 0 | |||
| 216 | + | } | |||
| 217 | + | delete(a.screens, id) | |||
| 218 | + | if err := s.release(); err != nil { | |||
| 219 | + | a.fail(fmt.Errorf("output %d removed: %w", global, err)) | |||
| 220 | + | } | |||
| 221 | + | } | |||
| 222 | + | } | |||
| 223 | + | ||||
| 191 | func (a *app) Locked() { | 224 | func (a *app) Locked() { | ||
| 192 | a.locked = true | 225 | a.locked = true | ||
| 193 | fmt.Println("locked") | 226 | fmt.Println("locked") | ||
| @@ -198,17 +231,30 @@ func (a *app) LockFinished() { | |||||
| 198 | } | 231 | } | ||
| 199 | 232 | ||||
| 200 | // FeedbackDone: dmabuf feedback → nefergui.RendererConfig. | 233 | // FeedbackDone: dmabuf feedback → nefergui.RendererConfig. | ||
| 234 | + | // | |||
| 235 | + | // Only the first complete feedback is used. A later one (the compositor | |||
| 236 | + | // changing its preferred device or formats) is ignored: this demo assumes a | |||
| 237 | + | // single GPU, and rebuilding the renderers is application policy that would | |||
| 238 | + | // obscure the glue. A real application would create a new Renderer here and | |||
| 239 | + | // re-import its buffers. | |||
| 201 | func (a *app) FeedbackDone(id neferclient.SurfaceID) { | 240 | func (a *app) FeedbackDone(id neferclient.SurfaceID) { | ||
| 202 | if s := a.screens[id]; s != nil { | 241 | if s := a.screens[id]; s != nil { | ||
| 203 | - | a.fail(s.setup()) | 242 | + | if err := s.setup(); err != nil { |
| 243 | + | a.fail(err) | |||
| 244 | + | } | |||
| 204 | } | 245 | } | ||
| 205 | } | 246 | } | ||
| 206 | 247 | ||||
| 207 | func (a *app) Configure(id neferclient.SurfaceID, _, _ int32) { | 248 | func (a *app) Configure(id neferclient.SurfaceID, _, _ int32) { | ||
| 208 | if s := a.screens[id]; s != nil { | 249 | if s := a.screens[id]; s != nil { | ||
| 209 | - | s.configured, s.canPresent = true, true | 250 | + | if !s.configured { // later configures must not lift the frame-callback gate |
| 251 | + | s.canPresent = true | |||
| 252 | + | } | |||
| 253 | + | s.configured = true | |||
| 210 | s.resize() | 254 | s.resize() | ||
| 211 | - | a.fail(s.setup()) | 255 | + | if err := s.setup(); err != nil { |
| 256 | + | a.fail(err) | |||
| 257 | + | } | |||
| 212 | } | 258 | } | ||
| 213 | } | 259 | } | ||
| 214 | 260 | ||||
| @@ -238,15 +284,26 @@ func (a *app) FDReady(id uint64) { | |||||
| 238 | 284 | ||||
| 239 | // Pointer: neferclient events → nefergui.Input, field by field. | 285 | // Pointer: neferclient events → nefergui.Input, field by field. | ||
| 240 | func (a *app) Pointer(ev *neferclient.PointerEvent) { | 286 | func (a *app) Pointer(ev *neferclient.PointerEvent) { | ||
| 287 | + | // Track the screen under the pointer first: the cursor shape is applied | |||
| 288 | + | // only from it. | |||
| 289 | + | switch ev.Kind { | |||
| 290 | + | case neferclient.PointerEnter: | |||
| 291 | + | a.hover = ev.Surface | |||
| 292 | + | // The shape only applies after an enter: send the last one again. | |||
| 293 | + | if err := a.seat.SetCursor(a.cursor); err != nil { | |||
| 294 | + | a.fail(fmt.Errorf("cursor: %w", err)) | |||
| 295 | + | } | |||
| 296 | + | case neferclient.PointerLeave: | |||
| 297 | + | if a.hover == ev.Surface { | |||
| 298 | + | a.hover = 0 | |||
| 299 | + | } | |||
| 300 | + | } | |||
| 241 | s := a.screens[ev.Surface] | 301 | s := a.screens[ev.Surface] | ||
| 242 | if s == nil || s.r == nil { | 302 | if s == nil || s.r == nil { | ||
| 243 | return | 303 | return | ||
| 244 | } | 304 | } | ||
| 245 | in := nefergui.Input{X: ev.X, Y: ev.Y, Kind: nefergui.InputPointerMotion} | 305 | in := nefergui.Input{X: ev.X, Y: ev.Y, Kind: nefergui.InputPointerMotion} | ||
| 246 | switch ev.Kind { | 306 | switch ev.Kind { | ||
| 247 | - | case neferclient.PointerEnter: | |||
| 248 | - | // The shape only applies after an enter: send the last one again. | |||
| 249 | - | a.fail(a.seat.SetCursor(a.cursor)) | |||
| 250 | case neferclient.PointerLeave: | 307 | case neferclient.PointerLeave: | ||
| 251 | in.Kind = nefergui.InputPointerLeave | 308 | in.Kind = nefergui.InputPointerLeave | ||
| 252 | case neferclient.PointerButton: | 309 | case neferclient.PointerButton: | ||
| @@ -274,12 +331,15 @@ func (a *app) Key(ev *neferclient.KeyEvent) { | |||||
| 274 | if s := a.screens[ev.Surface]; s != nil && s.r != nil { | 331 | if s := a.screens[ev.Surface]; s != nil && s.r != nil { | ||
| 275 | s.r.Input(&nefergui.Input{ | 332 | s.r.Input(&nefergui.Input{ | ||
| 276 | Kind: nefergui.InputKey, Keysym: ev.Keysym, Pressed: ev.Pressed, Repeat: ev.Repeat, | 333 | Kind: nefergui.InputKey, Keysym: ev.Keysym, Pressed: ev.Pressed, Repeat: ev.Repeat, | ||
| 277 | - | Modifiers: nefergui.Modifiers(ev.Modifiers & 0x0f), // shift, ctrl, alt, super | 334 | + | Modifiers: nefergui.Modifiers(ev.Modifiers & guiMods), |
| 278 | }) | 335 | }) | ||
| 279 | } | 336 | } | ||
| 280 | } | 337 | } | ||
| 281 | 338 | ||||
| 282 | func (a *app) KeyboardFocus(id neferclient.SurfaceID, focused bool) { | 339 | func (a *app) KeyboardFocus(id neferclient.SurfaceID, focused bool) { | ||
| 340 | + | if focused { | |||
| 341 | + | fmt.Println("focused") | |||
| 342 | + | } | |||
| 283 | if s := a.screens[id]; s != nil && s.r != nil { | 343 | if s := a.screens[id]; s != nil && s.r != nil { | ||
| 284 | kind := nefergui.InputFocusOut | 344 | kind := nefergui.InputFocusOut | ||
| 285 | if focused { | 345 | if focused { | ||
| @@ -376,9 +436,14 @@ func (s *screen) present() error { | |||||
| 376 | if err := s.conn.UnwatchFD(rt.ReleaseFD); err != nil { | 436 | if err := s.conn.UnwatchFD(rt.ReleaseFD); err != nil { | ||
| 377 | return fmt.Errorf("unwatch retired buffer %d: %w", rt.Buffer, err) | 437 | return fmt.Errorf("unwatch retired buffer %d: %w", rt.Buffer, err) | ||
| 378 | } | 438 | } | ||
| 439 | + | delete(s.live, rt.Buffer) | |||
| 379 | if err := s.surf.DestroyBuffer(rt.Buffer); err != nil { | 440 | if err := s.surf.DestroyBuffer(rt.Buffer); err != nil { | ||
| 380 | return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err) | 441 | return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err) | ||
| 381 | } | 442 | } | ||
| 443 | + | // The release timeline of a buffer is imported under the buffer's id. | |||
| 444 | + | if err := s.surf.DestroyTimeline(rt.Buffer); err != nil { | |||
| 445 | + | return fmt.Errorf("destroy retired timeline %d: %w", rt.Buffer, err) | |||
| 446 | + | } | |||
| 382 | } | 447 | } | ||
| 383 | if out.NewBuffer { | 448 | if out.NewBuffer { | ||
| 384 | buf := neferclient.Buffer{ | 449 | buf := neferclient.Buffer{ | ||
| @@ -396,6 +461,7 @@ func (s *screen) present() error { | |||||
| 396 | if err := s.conn.WatchFD(out.ReleaseFD, s.watchID(out.Buffer)); err != nil { | 461 | if err := s.conn.WatchFD(out.ReleaseFD, s.watchID(out.Buffer)); err != nil { | ||
| 397 | return fmt.Errorf("watch release fd: %w", err) | 462 | return fmt.Errorf("watch release fd: %w", err) | ||
| 398 | } | 463 | } | ||
| 464 | + | s.live[out.Buffer] = out.ReleaseFD | |||
| 399 | } | 465 | } | ||
| 400 | if out.NewTimelines { | 466 | if out.NewTimelines { | ||
| 401 | if !s.haveAcquire { // the acquire timeline is shared by every buffer | 467 | if !s.haveAcquire { // the acquire timeline is shared by every buffer | ||
| @@ -428,6 +494,20 @@ func (s *screen) present() error { | |||||
| 428 | return nil | 494 | return nil | ||
| 429 | } | 495 | } | ||
| 430 | 496 | ||||
| 497 | + | // release stops watching the live release eventfds, then frees the renderer and | |||
| 498 | + | // the surface (an output went away). The release eventfds belong to the | |||
| 499 | + | // renderer, so they are unwatched before it closes. | |||
| 500 | + | func (s *screen) release() error { | |||
| 501 | + | var errs []error | |||
| 502 | + | for buffer, fd := range s.live { | |||
| 503 | + | if err := s.conn.UnwatchFD(fd); err != nil { | |||
| 504 | + | errs = append(errs, fmt.Errorf("unwatch buffer %d: %w", buffer, err)) | |||
| 505 | + | } | |||
| 506 | + | } | |||
| 507 | + | clear(s.live) | |||
| 508 | + | return errors.Join(append(errs, s.close())...) | |||
| 509 | + | } | |||
| 510 | + | ||||
| 431 | // close frees the renderer and the surface. The session lock itself is only | 511 | // close frees the renderer and the surface. The session lock itself is only | ||
| 432 | // ended by Lock.Unlock. | 512 | // ended by Lock.Unlock. | ||
| 433 | func (s *screen) close() error { | 513 | func (s *screen) close() error { | ||