feat(layer): keyboard focus for interactive top/overlay layers, restored on unmap
- commit
- 6347722b19dc24d2b83a7794817388bed0328e41
- author
- brice · · 2026-09-24 09:42 UTC
- parent
- 039f0ba
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +110 -17
M internal/adapters/wayland/globals.go +2 -2
| @@ -302,9 +302,9 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) { | |||
| 302 | 302 | if r.Version() >= 4 { | |
| 303 | 303 | k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay)) | |
| 304 | 304 | } | |
| 305 | - | if w := s.windows[s.focused]; w != nil && w.mapped && w.xdg.resource.Client() == r.Client() { | |
| 305 | + | if surf, _ := s.focusTarget(s.focused); surf != nil && surf.Client() == r.Client() { | |
| 306 | 306 | s.serial++ | |
| 307 | - | k.SendEnter(s.serial, w.xdg.surfaceResource(), []byte{}) | |
| 307 | + | k.SendEnter(s.serial, surf, []byte{}) | |
| 308 | 308 | s.sendModifiers(k) | |
| 309 | 309 | } | |
| 310 | 310 | } | |
| @@ -302,9 +302,9 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) { | |||||
| 302 | if r.Version() >= 4 { | 302 | if r.Version() >= 4 { | ||
| 303 | k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay)) | 303 | k.SendRepeatInfo(int32(s.repeatRate), int32(s.repeatDelay)) | ||
| 304 | } | 304 | } | ||
| 305 | - | if w := s.windows[s.focused]; w != nil && w.mapped && w.xdg.resource.Client() == r.Client() { | 305 | + | if surf, _ := s.focusTarget(s.focused); surf != nil && surf.Client() == r.Client() { |
| 306 | s.serial++ | 306 | s.serial++ | ||
| 307 | - | k.SendEnter(s.serial, w.xdg.surfaceResource(), []byte{}) | 307 | + | k.SendEnter(s.serial, surf, []byte{}) |
| 308 | s.sendModifiers(k) | 308 | s.sendModifiers(k) | ||
| 309 | } | 309 | } | ||
| 310 | } | 310 | } | ||
M internal/adapters/wayland/layer.go +1 -2
| @@ -88,7 +88,6 @@ func (l *layerSurface) SetMargin(_ *wlrlayershell.ZwlrLayerSurfaceV1, t, r, b, l | |||
| 88 | 88 | l.pending.margin = [4]int32{t, r, b, left} | |
| 89 | 89 | } | |
| 90 | 90 | ||
| 91 | - | // Keyboard interactivity is deferred; layer-shell is capped at v4. | |
| 92 | 91 | func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfaceV1, k uint32) { | |
| 93 | 92 | if k > 2 { | |
| 94 | 93 | r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity), "invalid keyboard interactivity") | |
| @@ -137,7 +136,7 @@ func (s *Server) layerChanged() { | |||
| 137 | 136 | if !l.mapped { | |
| 138 | 137 | continue | |
| 139 | 138 | } | |
| 140 | - | v := ports.LayerSurface{ID: l.id, Layer: l.current.layer, Anchor: l.current.anchor, ExclusiveZone: l.current.zone, Margin: l.current.margin, Namespace: l.namespace} | |
| 139 | + | v := ports.LayerSurface{ID: l.id, Layer: l.current.layer, Anchor: l.current.anchor, ExclusiveZone: l.current.zone, Margin: l.current.margin, Namespace: l.namespace, Keyboard: l.current.keyboard} | |
| 141 | 140 | if l.surface.current != nil { | |
| 142 | 141 | if b := s.buffers[l.surface.current.Resource]; b != nil { | |
| 143 | 142 | v.Width, v.Height = b.width, b.height | |
| @@ -88,7 +88,6 @@ func (l *layerSurface) SetMargin(_ *wlrlayershell.ZwlrLayerSurfaceV1, t, r, b, l | |||||
| 88 | l.pending.margin = [4]int32{t, r, b, left} | 88 | l.pending.margin = [4]int32{t, r, b, left} | ||
| 89 | } | 89 | } | ||
| 90 | 90 | ||||
| 91 | - | // Keyboard interactivity is deferred; layer-shell is capped at v4. | |||
| 92 | func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfaceV1, k uint32) { | 91 | func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfaceV1, k uint32) { | ||
| 93 | if k > 2 { | 92 | if k > 2 { | ||
| 94 | r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity), "invalid keyboard interactivity") | 93 | r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity), "invalid keyboard interactivity") | ||
| @@ -137,7 +136,7 @@ func (s *Server) layerChanged() { | |||||
| 137 | if !l.mapped { | 136 | if !l.mapped { | ||
| 138 | continue | 137 | continue | ||
| 139 | } | 138 | } | ||
| 140 | - | v := ports.LayerSurface{ID: l.id, Layer: l.current.layer, Anchor: l.current.anchor, ExclusiveZone: l.current.zone, Margin: l.current.margin, Namespace: l.namespace} | 139 | + | v := ports.LayerSurface{ID: l.id, Layer: l.current.layer, Anchor: l.current.anchor, ExclusiveZone: l.current.zone, Margin: l.current.margin, Namespace: l.namespace, Keyboard: l.current.keyboard} |
| 141 | if l.surface.current != nil { | 140 | if l.surface.current != nil { | ||
| 142 | if b := s.buffers[l.surface.current.Resource]; b != nil { | 141 | if b := s.buffers[l.surface.current.Resource]; b != nil { | ||
| 143 | v.Width, v.Height = b.width, b.height | 142 | v.Width, v.Height = b.width, b.height | ||
M internal/adapters/wayland/server.go +33 -11
| @@ -346,8 +346,8 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 346 | 346 | s.changeFocus(c.ID) | |
| 347 | 347 | } | |
| 348 | 348 | case ports.ForwardKey: | |
| 349 | - | w := s.windows[c.ID] | |
| 350 | - | if c.ID != s.focused || w == nil || !w.mapped || len(s.windowKeyboards(w)) == 0 { | |
| 349 | + | _, keyboards := s.focusTarget(c.ID) | |
| 350 | + | if c.ID != s.focused || len(keyboards) == 0 { | |
| 351 | 351 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key") | |
| 352 | 352 | return | |
| 353 | 353 | } | |
| @@ -364,13 +364,13 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 364 | 364 | if c.Key.Pressed { | |
| 365 | 365 | state = 1 | |
| 366 | 366 | } | |
| 367 | - | for _, k := range s.windowKeyboards(w) { | |
| 367 | + | for _, k := range keyboards { | |
| 368 | 368 | k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state) | |
| 369 | 369 | } | |
| 370 | 370 | if c.Key.State != s.modState { | |
| 371 | 371 | s.modState = c.Key.State | |
| 372 | 372 | s.serial++ | |
| 373 | - | for _, k := range s.windowKeyboards(w) { | |
| 373 | + | for _, k := range keyboards { | |
| 374 | 374 | s.sendModifiers(k) | |
| 375 | 375 | } | |
| 376 | 376 | } | |
| @@ -397,8 +397,12 @@ func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | |||
| 397 | 397 | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | |
| 398 | 398 | return nil | |
| 399 | 399 | } | |
| 400 | + | return s.clientKeyboards(w.xdg.resource.Client()) | |
| 401 | + | } | |
| 402 | + | ||
| 403 | + | func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard { | |
| 400 | 404 | var alive []*wayland.Keyboard | |
| 401 | - | for _, k := range s.keyboards[w.xdg.resource.Client()] { | |
| 405 | + | for _, k := range s.keyboards[c] { | |
| 402 | 406 | if k.Resource.Alive() { | |
| 403 | 407 | alive = append(alive, k) | |
| 404 | 408 | } | |
| @@ -406,31 +410,49 @@ func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | |||
| 406 | 410 | return alive | |
| 407 | 411 | } | |
| 408 | 412 | ||
| 413 | + | // focusTarget resolves a window or layer ID to its wl_surface and keyboards. | |
| 414 | + | func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Keyboard) { | |
| 415 | + | if w := s.windows[id]; w != nil && w.mapped { | |
| 416 | + | if surf := w.xdg.surfaceResource(); surf != nil { | |
| 417 | + | return surf, s.windowKeyboards(w) | |
| 418 | + | } | |
| 419 | + | } | |
| 420 | + | if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() { | |
| 421 | + | for resource, state := range s.surfaces { | |
| 422 | + | if state == l.surface && resource.Alive() { | |
| 423 | + | return wayland.WrapSurface(resource), s.clientKeyboards(l.resource.Client()) | |
| 424 | + | } | |
| 425 | + | } | |
| 426 | + | } | |
| 427 | + | return nil, nil | |
| 428 | + | } | |
| 429 | + | ||
| 409 | 430 | func (s *Server) sendModifiers(k *wayland.Keyboard) { | |
| 410 | 431 | m := s.modState | |
| 411 | 432 | k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group) | |
| 412 | 433 | } | |
| 413 | 434 | ||
| 414 | 435 | func (s *Server) changeFocus(id ports.WindowID) { | |
| 415 | - | if old := s.windows[s.focused]; old != nil && old.xdg.surfaceResource() != nil { | |
| 416 | - | for _, k := range s.windowKeyboards(old) { | |
| 436 | + | if surf, keyboards := s.focusTarget(s.focused); surf != nil { | |
| 437 | + | for _, k := range keyboards { | |
| 417 | 438 | s.serial++ | |
| 418 | - | k.SendLeave(s.serial, old.xdg.surfaceResource()) | |
| 439 | + | k.SendLeave(s.serial, surf) | |
| 419 | 440 | } | |
| 420 | 441 | } | |
| 421 | 442 | s.focused = 0 | |
| 422 | - | if w := s.windows[id]; w != nil && w.mapped && w.xdg.surfaceResource() != nil { | |
| 443 | + | if surf, keyboards := s.focusTarget(id); surf != nil { | |
| 423 | 444 | s.focused = id | |
| 424 | - | for _, k := range s.windowKeyboards(w) { | |
| 445 | + | for _, k := range keyboards { | |
| 425 | 446 | s.serial++ | |
| 426 | 447 | var keys []byte | |
| 427 | 448 | for code := range s.heldKeys { | |
| 428 | 449 | keys = binary.LittleEndian.AppendUint32(keys, code) | |
| 429 | 450 | } | |
| 430 | - | k.SendEnter(s.serial, w.xdg.surfaceResource(), keys) | |
| 451 | + | k.SendEnter(s.serial, surf, keys) | |
| 431 | 452 | s.sendModifiers(k) | |
| 432 | 453 | } | |
| 433 | 454 | } | |
| 455 | + | s.log.Debug().Uint64("id", uint64(s.focused)).Msg("keyboard focus") | |
| 434 | 456 | } | |
| 435 | 457 | ||
| 436 | 458 | func pointerFrame(p *wayland.Pointer) { | |
| @@ -346,8 +346,8 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 346 | s.changeFocus(c.ID) | 346 | s.changeFocus(c.ID) | ||
| 347 | } | 347 | } | ||
| 348 | case ports.ForwardKey: | 348 | case ports.ForwardKey: | ||
| 349 | - | w := s.windows[c.ID] | 349 | + | _, keyboards := s.focusTarget(c.ID) |
| 350 | - | if c.ID != s.focused || w == nil || !w.mapped || len(s.windowKeyboards(w)) == 0 { | 350 | + | if c.ID != s.focused || len(keyboards) == 0 { |
| 351 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key") | 351 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored forward key") | ||
| 352 | return | 352 | return | ||
| 353 | } | 353 | } | ||
| @@ -364,13 +364,13 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 364 | if c.Key.Pressed { | 364 | if c.Key.Pressed { | ||
| 365 | state = 1 | 365 | state = 1 | ||
| 366 | } | 366 | } | ||
| 367 | - | for _, k := range s.windowKeyboards(w) { | 367 | + | for _, k := range keyboards { |
| 368 | k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state) | 368 | k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state) | ||
| 369 | } | 369 | } | ||
| 370 | if c.Key.State != s.modState { | 370 | if c.Key.State != s.modState { | ||
| 371 | s.modState = c.Key.State | 371 | s.modState = c.Key.State | ||
| 372 | s.serial++ | 372 | s.serial++ | ||
| 373 | - | for _, k := range s.windowKeyboards(w) { | 373 | + | for _, k := range keyboards { |
| 374 | s.sendModifiers(k) | 374 | s.sendModifiers(k) | ||
| 375 | } | 375 | } | ||
| 376 | } | 376 | } | ||
| @@ -397,8 +397,12 @@ func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | |||||
| 397 | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | 397 | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | ||
| 398 | return nil | 398 | return nil | ||
| 399 | } | 399 | } | ||
| 400 | + | return s.clientKeyboards(w.xdg.resource.Client()) | |||
| 401 | + | } | |||
| 402 | + | ||||
| 403 | + | func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard { | |||
| 400 | var alive []*wayland.Keyboard | 404 | var alive []*wayland.Keyboard | ||
| 401 | - | for _, k := range s.keyboards[w.xdg.resource.Client()] { | 405 | + | for _, k := range s.keyboards[c] { |
| 402 | if k.Resource.Alive() { | 406 | if k.Resource.Alive() { | ||
| 403 | alive = append(alive, k) | 407 | alive = append(alive, k) | ||
| 404 | } | 408 | } | ||
| @@ -406,31 +410,49 @@ func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | |||||
| 406 | return alive | 410 | return alive | ||
| 407 | } | 411 | } | ||
| 408 | 412 | ||||
| 413 | + | // focusTarget resolves a window or layer ID to its wl_surface and keyboards. | |||
| 414 | + | func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Keyboard) { | |||
| 415 | + | if w := s.windows[id]; w != nil && w.mapped { | |||
| 416 | + | if surf := w.xdg.surfaceResource(); surf != nil { | |||
| 417 | + | return surf, s.windowKeyboards(w) | |||
| 418 | + | } | |||
| 419 | + | } | |||
| 420 | + | if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() { | |||
| 421 | + | for resource, state := range s.surfaces { | |||
| 422 | + | if state == l.surface && resource.Alive() { | |||
| 423 | + | return wayland.WrapSurface(resource), s.clientKeyboards(l.resource.Client()) | |||
| 424 | + | } | |||
| 425 | + | } | |||
| 426 | + | } | |||
| 427 | + | return nil, nil | |||
| 428 | + | } | |||
| 429 | + | ||||
| 409 | func (s *Server) sendModifiers(k *wayland.Keyboard) { | 430 | func (s *Server) sendModifiers(k *wayland.Keyboard) { | ||
| 410 | m := s.modState | 431 | m := s.modState | ||
| 411 | k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group) | 432 | k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group) | ||
| 412 | } | 433 | } | ||
| 413 | 434 | ||||
| 414 | func (s *Server) changeFocus(id ports.WindowID) { | 435 | func (s *Server) changeFocus(id ports.WindowID) { | ||
| 415 | - | if old := s.windows[s.focused]; old != nil && old.xdg.surfaceResource() != nil { | 436 | + | if surf, keyboards := s.focusTarget(s.focused); surf != nil { |
| 416 | - | for _, k := range s.windowKeyboards(old) { | 437 | + | for _, k := range keyboards { |
| 417 | s.serial++ | 438 | s.serial++ | ||
| 418 | - | k.SendLeave(s.serial, old.xdg.surfaceResource()) | 439 | + | k.SendLeave(s.serial, surf) |
| 419 | } | 440 | } | ||
| 420 | } | 441 | } | ||
| 421 | s.focused = 0 | 442 | s.focused = 0 | ||
| 422 | - | if w := s.windows[id]; w != nil && w.mapped && w.xdg.surfaceResource() != nil { | 443 | + | if surf, keyboards := s.focusTarget(id); surf != nil { |
| 423 | s.focused = id | 444 | s.focused = id | ||
| 424 | - | for _, k := range s.windowKeyboards(w) { | 445 | + | for _, k := range keyboards { |
| 425 | s.serial++ | 446 | s.serial++ | ||
| 426 | var keys []byte | 447 | var keys []byte | ||
| 427 | for code := range s.heldKeys { | 448 | for code := range s.heldKeys { | ||
| 428 | keys = binary.LittleEndian.AppendUint32(keys, code) | 449 | keys = binary.LittleEndian.AppendUint32(keys, code) | ||
| 429 | } | 450 | } | ||
| 430 | - | k.SendEnter(s.serial, w.xdg.surfaceResource(), keys) | 451 | + | k.SendEnter(s.serial, surf, keys) |
| 431 | s.sendModifiers(k) | 452 | s.sendModifiers(k) | ||
| 432 | } | 453 | } | ||
| 433 | } | 454 | } | ||
| 455 | + | s.log.Debug().Uint64("id", uint64(s.focused)).Msg("keyboard focus") | |||
| 434 | } | 456 | } | ||
| 435 | 457 | ||||
| 436 | func pointerFrame(p *wayland.Pointer) { | 458 | func pointerFrame(p *wayland.Pointer) { | ||
M internal/core/core.go +19 -2
| @@ -161,11 +161,28 @@ func (c *Core) clientRect(p Placement) Rect { | |||
| 161 | 161 | return Rect{X: r.X + b, Y: r.Y + b, W: r.W - 2*b, H: r.H - 2*b} | |
| 162 | 162 | } | |
| 163 | 163 | ||
| 164 | + | // keyboardFocus is the newest mapped top/overlay layer that asks for the keyboard | |
| 165 | + | // (exclusive or on-demand), else the focused window. When the layer unmaps, | |
| 166 | + | // focus returns to the window. | |
| 167 | + | func (c *Core) keyboardFocus() WindowID { | |
| 168 | + | var layer WindowID | |
| 169 | + | for _, l := range c.layers { | |
| 170 | + | if l.Keyboard != 0 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > layer { | |
| 171 | + | layer = l.ID | |
| 172 | + | } | |
| 173 | + | } | |
| 174 | + | if layer != 0 { | |
| 175 | + | return layer | |
| 176 | + | } | |
| 177 | + | id, _ := c.ws.Focused() | |
| 178 | + | return id | |
| 179 | + | } | |
| 180 | + | ||
| 164 | 181 | func (c *Core) publish(ctx context.Context) error { | |
| 165 | 182 | c.seq++ | |
| 166 | 183 | scene := ports.Scene{Seq: c.seq, OutputWidth: c.ws.Output.W, OutputHeight: c.ws.Output.H, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: append([]ports.SceneLayer(nil), c.placed...)} | |
| 167 | 184 | alive := map[WindowID]bool{} | |
| 168 | - | focus, _ := c.ws.Focused() | |
| 185 | + | focus := c.keyboardFocus() | |
| 169 | 186 | for _, p := range c.ws.Layout() { | |
| 170 | 187 | alive[p.ID] = true | |
| 171 | 188 | scene.Windows = append(scene.Windows, ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: p.Focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden}) | |
| @@ -390,7 +407,7 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 390 | 407 | } | |
| 391 | 408 | c.pressed[name] = false | |
| 392 | 409 | } | |
| 393 | - | if id, ok := c.ws.Focused(); ok { | |
| 410 | + | if id := c.keyboardFocus(); id != 0 { | |
| 394 | 411 | if err := c.command(ctx, ports.ForwardKey{ID: id, Key: key}); err != nil { | |
| 395 | 412 | return nil | |
| 396 | 413 | } | |
| @@ -161,11 +161,28 @@ func (c *Core) clientRect(p Placement) Rect { | |||||
| 161 | return Rect{X: r.X + b, Y: r.Y + b, W: r.W - 2*b, H: r.H - 2*b} | 161 | return Rect{X: r.X + b, Y: r.Y + b, W: r.W - 2*b, H: r.H - 2*b} | ||
| 162 | } | 162 | } | ||
| 163 | 163 | ||||
| 164 | + | // keyboardFocus is the newest mapped top/overlay layer that asks for the keyboard | |||
| 165 | + | // (exclusive or on-demand), else the focused window. When the layer unmaps, | |||
| 166 | + | // focus returns to the window. | |||
| 167 | + | func (c *Core) keyboardFocus() WindowID { | |||
| 168 | + | var layer WindowID | |||
| 169 | + | for _, l := range c.layers { | |||
| 170 | + | if l.Keyboard != 0 && (l.Layer == ports.LayerTop || l.Layer == ports.LayerOverlay) && l.ID > layer { | |||
| 171 | + | layer = l.ID | |||
| 172 | + | } | |||
| 173 | + | } | |||
| 174 | + | if layer != 0 { | |||
| 175 | + | return layer | |||
| 176 | + | } | |||
| 177 | + | id, _ := c.ws.Focused() | |||
| 178 | + | return id | |||
| 179 | + | } | |||
| 180 | + | ||||
| 164 | func (c *Core) publish(ctx context.Context) error { | 181 | func (c *Core) publish(ctx context.Context) error { | ||
| 165 | c.seq++ | 182 | c.seq++ | ||
| 166 | scene := ports.Scene{Seq: c.seq, OutputWidth: c.ws.Output.W, OutputHeight: c.ws.Output.H, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: append([]ports.SceneLayer(nil), c.placed...)} | 183 | scene := ports.Scene{Seq: c.seq, OutputWidth: c.ws.Output.W, OutputHeight: c.ws.Output.H, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: append([]ports.SceneLayer(nil), c.placed...)} | ||
| 167 | alive := map[WindowID]bool{} | 184 | alive := map[WindowID]bool{} | ||
| 168 | - | focus, _ := c.ws.Focused() | 185 | + | focus := c.keyboardFocus() |
| 169 | for _, p := range c.ws.Layout() { | 186 | for _, p := range c.ws.Layout() { | ||
| 170 | alive[p.ID] = true | 187 | alive[p.ID] = true | ||
| 171 | scene.Windows = append(scene.Windows, ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: p.Focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden}) | 188 | scene.Windows = append(scene.Windows, ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: p.Focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden}) | ||
| @@ -390,7 +407,7 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 390 | } | 407 | } | ||
| 391 | c.pressed[name] = false | 408 | c.pressed[name] = false | ||
| 392 | } | 409 | } | ||
| 393 | - | if id, ok := c.ws.Focused(); ok { | 410 | + | if id := c.keyboardFocus(); id != 0 { |
| 394 | if err := c.command(ctx, ports.ForwardKey{ID: id, Key: key}); err != nil { | 411 | if err := c.command(ctx, ports.ForwardKey{ID: id, Key: key}); err != nil { | ||
| 395 | return nil | 412 | return nil | ||
| 396 | } | 413 | } | ||
M internal/core/core_test.go +53 -0
| @@ -282,3 +282,56 @@ func TestBorderInset(t *testing.T) { | |||
| 282 | 282 | t.Fatal(v) | |
| 283 | 283 | } | |
| 284 | 284 | } | |
| 285 | + | ||
| 286 | + | func TestLayerKeyboardFocus(t *testing.T) { | |
| 287 | + | cfg := config.Defaults() | |
| 288 | + | client := make(chan ports.ClientEvent, 8) | |
| 289 | + | input := make(chan ports.InputEvent, 8) | |
| 290 | + | output := make(chan ports.OutputEvent, 8) | |
| 291 | + | commands := make(chan ports.ClientCommand, 64) | |
| 292 | + | scenes := make(chan ports.Scene, 1) | |
| 293 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 294 | + | if err != nil { | |
| 295 | + | t.Fatal(err) | |
| 296 | + | } | |
| 297 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 298 | + | defer cancel() | |
| 299 | + | go c.Run(ctx) | |
| 300 | + | output <- ports.OutputMode{Width: 100, Height: 80} | |
| 301 | + | receive(t, scenes) | |
| 302 | + | client <- ports.WindowMapped{ID: 1} | |
| 303 | + | receive(t, scenes) | |
| 304 | + | focusOf := func() ports.WindowID { | |
| 305 | + | t.Helper() | |
| 306 | + | for { | |
| 307 | + | if v, ok := receive(t, commands).(ports.FocusWindow); ok { | |
| 308 | + | return v.ID | |
| 309 | + | } | |
| 310 | + | } | |
| 311 | + | } | |
| 312 | + | if id := focusOf(); id != 1 { | |
| 313 | + | t.Fatalf("window focus %d", id) | |
| 314 | + | } | |
| 315 | + | // A bar without keyboard interactivity does not steal focus; a launcher does. | |
| 316 | + | launcher := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Width: 40, Height: 20, Keyboard: 1} | |
| 317 | + | bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 5} | |
| 318 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar, launcher}} | |
| 319 | + | receive(t, scenes) | |
| 320 | + | if id := focusOf(); id != 7 { | |
| 321 | + | t.Fatalf("layer focus %d", id) | |
| 322 | + | } | |
| 323 | + | input <- ports.KeyEvent{Keysym: "a", Keycode: 30, Pressed: true} | |
| 324 | + | for { | |
| 325 | + | if v, ok := receive(t, commands).(ports.ForwardKey); ok { | |
| 326 | + | if v.ID != 7 { | |
| 327 | + | t.Fatalf("key went to %d", v.ID) | |
| 328 | + | } | |
| 329 | + | break | |
| 330 | + | } | |
| 331 | + | } | |
| 332 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}} | |
| 333 | + | receive(t, scenes) | |
| 334 | + | if id := focusOf(); id != 1 { | |
| 335 | + | t.Fatalf("focus not restored: %d", id) | |
| 336 | + | } | |
| 337 | + | } | |
| @@ -282,3 +282,56 @@ func TestBorderInset(t *testing.T) { | |||||
| 282 | t.Fatal(v) | 282 | t.Fatal(v) | ||
| 283 | } | 283 | } | ||
| 284 | } | 284 | } | ||
| 285 | + | ||||
| 286 | + | func TestLayerKeyboardFocus(t *testing.T) { | |||
| 287 | + | cfg := config.Defaults() | |||
| 288 | + | client := make(chan ports.ClientEvent, 8) | |||
| 289 | + | input := make(chan ports.InputEvent, 8) | |||
| 290 | + | output := make(chan ports.OutputEvent, 8) | |||
| 291 | + | commands := make(chan ports.ClientCommand, 64) | |||
| 292 | + | scenes := make(chan ports.Scene, 1) | |||
| 293 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |||
| 294 | + | if err != nil { | |||
| 295 | + | t.Fatal(err) | |||
| 296 | + | } | |||
| 297 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 298 | + | defer cancel() | |||
| 299 | + | go c.Run(ctx) | |||
| 300 | + | output <- ports.OutputMode{Width: 100, Height: 80} | |||
| 301 | + | receive(t, scenes) | |||
| 302 | + | client <- ports.WindowMapped{ID: 1} | |||
| 303 | + | receive(t, scenes) | |||
| 304 | + | focusOf := func() ports.WindowID { | |||
| 305 | + | t.Helper() | |||
| 306 | + | for { | |||
| 307 | + | if v, ok := receive(t, commands).(ports.FocusWindow); ok { | |||
| 308 | + | return v.ID | |||
| 309 | + | } | |||
| 310 | + | } | |||
| 311 | + | } | |||
| 312 | + | if id := focusOf(); id != 1 { | |||
| 313 | + | t.Fatalf("window focus %d", id) | |||
| 314 | + | } | |||
| 315 | + | // A bar without keyboard interactivity does not steal focus; a launcher does. | |||
| 316 | + | launcher := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Width: 40, Height: 20, Keyboard: 1} | |||
| 317 | + | bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 5} | |||
| 318 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar, launcher}} | |||
| 319 | + | receive(t, scenes) | |||
| 320 | + | if id := focusOf(); id != 7 { | |||
| 321 | + | t.Fatalf("layer focus %d", id) | |||
| 322 | + | } | |||
| 323 | + | input <- ports.KeyEvent{Keysym: "a", Keycode: 30, Pressed: true} | |||
| 324 | + | for { | |||
| 325 | + | if v, ok := receive(t, commands).(ports.ForwardKey); ok { | |||
| 326 | + | if v.ID != 7 { | |||
| 327 | + | t.Fatalf("key went to %d", v.ID) | |||
| 328 | + | } | |||
| 329 | + | break | |||
| 330 | + | } | |||
| 331 | + | } | |||
| 332 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}} | |||
| 333 | + | receive(t, scenes) | |||
| 334 | + | if id := focusOf(); id != 1 { | |||
| 335 | + | t.Fatalf("focus not restored: %d", id) | |||
| 336 | + | } | |||
| 337 | + | } | |||
M internal/ports/messages.go +2 -0
| @@ -53,6 +53,8 @@ type LayerSurface struct { | |||
| 53 | 53 | Margin [4]int32 | |
| 54 | 54 | Width, Height int | |
| 55 | 55 | Namespace string | |
| 56 | + | // Keyboard is the requested interactivity: 0 none, 1 exclusive, 2 on-demand. | |
| 57 | + | Keyboard uint32 | |
| 56 | 58 | } | |
| 57 | 59 | ||
| 58 | 60 | // LayerChanged carries wayland → core the full list of mapped layer surfaces, | |
| @@ -53,6 +53,8 @@ type LayerSurface struct { | |||||
| 53 | Margin [4]int32 | 53 | Margin [4]int32 | ||
| 54 | Width, Height int | 54 | Width, Height int | ||
| 55 | Namespace string | 55 | Namespace string | ||
| 56 | + | // Keyboard is the requested interactivity: 0 none, 1 exclusive, 2 on-demand. | |||
| 57 | + | Keyboard uint32 | |||
| 56 | } | 58 | } | ||
| 57 | 59 | ||||
| 58 | // LayerChanged carries wayland → core the full list of mapped layer surfaces, | 60 | // LayerChanged carries wayland → core the full list of mapped layer surfaces, | ||