fix(layer): on-demand focus survives the pointer moving to another output
- commit
- 16be8cb34587bbe5f77a71b9d2aae42645319a03
- author
- brice · · 2026-09-26 06:03 UTC
- committer
- brice · · 2026-09-26 06:05 UTC
- parent
- e2fd4a0
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +50 -4
M internal/core/core.go +11 -4
| @@ -87,6 +87,9 @@ type Core struct { | |||
| 87 | 87 | // new window takes the keyboard back. | |
| 88 | 88 | layerFocus WindowID | |
| 89 | 89 | layerOver WindowID | |
| 90 | + | // layerScreen is the output focused at the click: the pointer moving to | |
| 91 | + | // another output does not end the layer's focus. | |
| 92 | + | layerScreen *screen | |
| 90 | 93 | } | |
| 91 | 94 | ||
| 92 | 95 | func keyName(s string) string { | |
| @@ -322,13 +325,16 @@ func (c *Core) keyboardFocus() WindowID { | |||
| 322 | 325 | if g := c.grabFocus(); g != 0 { | |
| 323 | 326 | return g | |
| 324 | 327 | } | |
| 325 | - | id, _ := c.cur().mon.Focused() | |
| 326 | - | if c.layerFocus != 0 && (!c.onDemand(c.layerFocus) || id != c.layerOver) { | |
| 327 | - | c.layerFocus = 0 | |
| 328 | + | if c.layerFocus != 0 { | |
| 329 | + | over, _ := c.layerScreen.mon.Focused() | |
| 330 | + | if !c.onDemand(c.layerFocus) || !slices.Contains(c.screens, c.layerScreen) || over != c.layerOver { | |
| 331 | + | c.layerFocus, c.layerScreen = 0, nil | |
| 332 | + | } | |
| 328 | 333 | } | |
| 329 | 334 | if c.layerFocus != 0 { | |
| 330 | 335 | return c.layerFocus | |
| 331 | 336 | } | |
| 337 | + | id, _ := c.cur().mon.Focused() | |
| 332 | 338 | return id | |
| 333 | 339 | } | |
| 334 | 340 | ||
| @@ -747,7 +753,8 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 747 | 753 | // A click on an on-demand layer gives it the keyboard. | |
| 748 | 754 | if v.Pressed && c.onDemand(id) && c.layerFocus != id { | |
| 749 | 755 | c.layerFocus = id | |
| 750 | - | c.layerOver, _ = c.cur().mon.Focused() | |
| 756 | + | c.layerScreen = c.cur() | |
| 757 | + | c.layerOver, _ = c.layerScreen.mon.Focused() | |
| 751 | 758 | if err := c.publish(ctx); err != nil { | |
| 752 | 759 | return nil | |
| 753 | 760 | } | |
| @@ -87,6 +87,9 @@ type Core struct { | |||||
| 87 | // new window takes the keyboard back. | 87 | // new window takes the keyboard back. | ||
| 88 | layerFocus WindowID | 88 | layerFocus WindowID | ||
| 89 | layerOver WindowID | 89 | layerOver WindowID | ||
| 90 | + | // layerScreen is the output focused at the click: the pointer moving to | |||
| 91 | + | // another output does not end the layer's focus. | |||
| 92 | + | layerScreen *screen | |||
| 90 | } | 93 | } | ||
| 91 | 94 | ||||
| 92 | func keyName(s string) string { | 95 | func keyName(s string) string { | ||
| @@ -322,13 +325,16 @@ func (c *Core) keyboardFocus() WindowID { | |||||
| 322 | if g := c.grabFocus(); g != 0 { | 325 | if g := c.grabFocus(); g != 0 { | ||
| 323 | return g | 326 | return g | ||
| 324 | } | 327 | } | ||
| 325 | - | id, _ := c.cur().mon.Focused() | 328 | + | if c.layerFocus != 0 { |
| 326 | - | if c.layerFocus != 0 && (!c.onDemand(c.layerFocus) || id != c.layerOver) { | 329 | + | over, _ := c.layerScreen.mon.Focused() |
| 327 | - | c.layerFocus = 0 | 330 | + | if !c.onDemand(c.layerFocus) || !slices.Contains(c.screens, c.layerScreen) || over != c.layerOver { |
| 331 | + | c.layerFocus, c.layerScreen = 0, nil | |||
| 332 | + | } | |||
| 328 | } | 333 | } | ||
| 329 | if c.layerFocus != 0 { | 334 | if c.layerFocus != 0 { | ||
| 330 | return c.layerFocus | 335 | return c.layerFocus | ||
| 331 | } | 336 | } | ||
| 337 | + | id, _ := c.cur().mon.Focused() | |||
| 332 | return id | 338 | return id | ||
| 333 | } | 339 | } | ||
| 334 | 340 | ||||
| @@ -747,7 +753,8 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 747 | // A click on an on-demand layer gives it the keyboard. | 753 | // A click on an on-demand layer gives it the keyboard. | ||
| 748 | if v.Pressed && c.onDemand(id) && c.layerFocus != id { | 754 | if v.Pressed && c.onDemand(id) && c.layerFocus != id { | ||
| 749 | c.layerFocus = id | 755 | c.layerFocus = id | ||
| 750 | - | c.layerOver, _ = c.cur().mon.Focused() | 756 | + | c.layerScreen = c.cur() |
| 757 | + | c.layerOver, _ = c.layerScreen.mon.Focused() | |||
| 751 | if err := c.publish(ctx); err != nil { | 758 | if err := c.publish(ctx); err != nil { | ||
| 752 | return nil | 759 | return nil | ||
| 753 | } | 760 | } | ||
M internal/core/layer_input_test.go +39 -0
| @@ -220,3 +220,42 @@ func TestLayerHitOrder(t *testing.T) { | |||
| 220 | 220 | hit(50, 5, 1) | |
| 221 | 221 | hit(50, 75, 7) | |
| 222 | 222 | } | |
| 223 | + | ||
| 224 | + | // The pointer moving to another output does not take the keyboard from a | |
| 225 | + | // clicked on-demand layer: keys still reach it. | |
| 226 | + | func TestLayerFocusAcrossOutputs(t *testing.T) { | |
| 227 | + | cfg := config.Defaults() | |
| 228 | + | cfg.Border.Width = 0 | |
| 229 | + | client := make(chan ports.ClientEvent, 8) | |
| 230 | + | input := make(chan ports.InputEvent, 8) | |
| 231 | + | output := make(chan ports.OutputEvent, 8) | |
| 232 | + | commands := make(chan ports.ClientCommand, 256) | |
| 233 | + | scenes := make(chan []ports.Scene, 1) | |
| 234 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 235 | + | if err != nil { | |
| 236 | + | t.Fatal(err) | |
| 237 | + | } | |
| 238 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 239 | + | defer cancel() | |
| 240 | + | go c.Run(ctx) | |
| 241 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |
| 242 | + | scene(t, scenes) | |
| 243 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-2", Width: 100, Height: 80}} | |
| 244 | + | scene(t, scenes) | |
| 245 | + | client <- ports.WindowMapped{ID: 1} | |
| 246 | + | scene(t, scenes) | |
| 247 | + | bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 10, Keyboard: 2, Output: "OUT-1"} | |
| 248 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}} | |
| 249 | + | scene(t, scenes) | |
| 250 | + | input <- ports.PointerMotion{X: 50, Y: 5} | |
| 251 | + | next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 6 }) | |
| 252 | + | input <- ports.PointerButton{Button: 0x110, Pressed: true} | |
| 253 | + | next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 6 }) | |
| 254 | + | input <- ports.PointerButton{Button: 0x110} | |
| 255 | + | input <- ports.PointerMotion{X: 100, Y: 40} // onto OUT-2, now focused | |
| 256 | + | next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 0 }) | |
| 257 | + | input <- ports.KeyEvent{Keysym: "a", Pressed: true} | |
| 258 | + | if v := next(t, commands, anyOf[ports.ForwardKey]); v.ID != 6 { | |
| 259 | + | t.Fatalf("key to %d, want the bar", v.ID) | |
| 260 | + | } | |
| 261 | + | } | |
| @@ -220,3 +220,42 @@ func TestLayerHitOrder(t *testing.T) { | |||||
| 220 | hit(50, 5, 1) | 220 | hit(50, 5, 1) | ||
| 221 | hit(50, 75, 7) | 221 | hit(50, 75, 7) | ||
| 222 | } | 222 | } | ||
| 223 | + | ||||
| 224 | + | // The pointer moving to another output does not take the keyboard from a | |||
| 225 | + | // clicked on-demand layer: keys still reach it. | |||
| 226 | + | func TestLayerFocusAcrossOutputs(t *testing.T) { | |||
| 227 | + | cfg := config.Defaults() | |||
| 228 | + | cfg.Border.Width = 0 | |||
| 229 | + | client := make(chan ports.ClientEvent, 8) | |||
| 230 | + | input := make(chan ports.InputEvent, 8) | |||
| 231 | + | output := make(chan ports.OutputEvent, 8) | |||
| 232 | + | commands := make(chan ports.ClientCommand, 256) | |||
| 233 | + | scenes := make(chan []ports.Scene, 1) | |||
| 234 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |||
| 235 | + | if err != nil { | |||
| 236 | + | t.Fatal(err) | |||
| 237 | + | } | |||
| 238 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 239 | + | defer cancel() | |||
| 240 | + | go c.Run(ctx) | |||
| 241 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |||
| 242 | + | scene(t, scenes) | |||
| 243 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-2", Width: 100, Height: 80}} | |||
| 244 | + | scene(t, scenes) | |||
| 245 | + | client <- ports.WindowMapped{ID: 1} | |||
| 246 | + | scene(t, scenes) | |||
| 247 | + | bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 10, Keyboard: 2, Output: "OUT-1"} | |||
| 248 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}} | |||
| 249 | + | scene(t, scenes) | |||
| 250 | + | input <- ports.PointerMotion{X: 50, Y: 5} | |||
| 251 | + | next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 6 }) | |||
| 252 | + | input <- ports.PointerButton{Button: 0x110, Pressed: true} | |||
| 253 | + | next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 6 }) | |||
| 254 | + | input <- ports.PointerButton{Button: 0x110} | |||
| 255 | + | input <- ports.PointerMotion{X: 100, Y: 40} // onto OUT-2, now focused | |||
| 256 | + | next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 0 }) | |||
| 257 | + | input <- ports.KeyEvent{Keysym: "a", Pressed: true} | |||
| 258 | + | if v := next(t, commands, anyOf[ports.ForwardKey]); v.ID != 6 { | |||
| 259 | + | t.Fatalf("key to %d, want the bar", v.ID) | |||
| 260 | + | } | |||
| 261 | + | } | |||