fix(core): keep fullscreen cover when a stashed window is activated
Activating a hidden stashed window (xdg-activation, taskbar) ended the covering window's fullscreen, sending a game a 0x0 configure; it quit. The stash now shows over the cover, as toggle-stash-visible does. Foreign toplevel activations are logged.
- commit
- 699102b47ae07ee62042fe3dc64ef506ee7d948e
- author
- brice · · 2026-10-12 18:29 UTC
- parent
- 446d1e3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +61 -14
M internal/adapters/wayland/foreigntoplevel.go +1 -0
| @@ -267,6 +267,7 @@ func (*toplevelRequests) Destroy(*wlr.ZwlrForeignToplevelHandleV1) {} | |||
| 267 | 267 | ||
| 268 | 268 | func (t *toplevelRequests) Activate(*wlr.ZwlrForeignToplevelHandleV1, *wayland.Seat) { | |
| 269 | 269 | if w := t.window(); w != nil { | |
| 270 | + | t.s.log.Info().Uint64("id", uint64(w.id)).Msg("foreign toplevel activation") | |
| 270 | 271 | t.s.emit(ports.WindowActivate{ID: w.id}) | |
| 271 | 272 | } | |
| 272 | 273 | } | |
| @@ -267,6 +267,7 @@ func (*toplevelRequests) Destroy(*wlr.ZwlrForeignToplevelHandleV1) {} | |||||
| 267 | 267 | ||||
| 268 | func (t *toplevelRequests) Activate(*wlr.ZwlrForeignToplevelHandleV1, *wayland.Seat) { | 268 | func (t *toplevelRequests) Activate(*wlr.ZwlrForeignToplevelHandleV1, *wayland.Seat) { | ||
| 269 | if w := t.window(); w != nil { | 269 | if w := t.window(); w != nil { | ||
| 270 | + | t.s.log.Info().Uint64("id", uint64(w.id)).Msg("foreign toplevel activation") | |||
| 270 | t.s.emit(ports.WindowActivate{ID: w.id}) | 271 | t.s.emit(ports.WindowActivate{ID: w.id}) | ||
| 271 | } | 272 | } | ||
| 272 | } | 273 | } | ||
M internal/core/floating_test.go +32 -4
| @@ -490,8 +490,9 @@ func TestFloatingSceneDimAndConfigure(t *testing.T) { | |||
| 490 | 490 | check(0.3) | |
| 491 | 491 | } | |
| 492 | 492 | ||
| 493 | - | // Activating a hidden stashed window shows the stash and focuses it; the | |
| 494 | - | // fullscreen window it hid behind leaves fullscreen. | |
| 493 | + | // Activating a hidden stashed window shows the stash over the covering | |
| 494 | + | // window and focuses it; the cover stays fullscreen behind and returns | |
| 495 | + | // when the stash hides. | |
| 495 | 496 | func TestActivateHiddenFloat(t *testing.T) { | |
| 496 | 497 | w := workspace() | |
| 497 | 498 | w.AddWindow(1) | |
| @@ -501,9 +502,15 @@ func TestActivateHiddenFloat(t *testing.T) { | |||
| 501 | 502 | w.FocusID(1) | |
| 502 | 503 | w.SetFullscreen(1, true) | |
| 503 | 504 | w.Activate(2) | |
| 504 | - | if id, _ := w.Focused(); id != 2 || w.stashHidden || w.fullscreen != 0 { | |
| 505 | - | t.Fatalf("focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen) | |
| 505 | + | if id, _ := w.Focused(); id != 2 || !w.stashShown() || w.fullscreen != 1 { | |
| 506 | + | t.Fatalf("focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen) | |
| 506 | 507 | } | |
| 508 | + | w.ToggleStashVisible() | |
| 509 | + | if id, _ := w.Focused(); id != 1 || w.stashShown() || w.fullscreen != 1 { | |
| 510 | + | t.Fatalf("stash hidden: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen) | |
| 511 | + | } | |
| 512 | + | w.SetFullscreen(1, false) | |
| 513 | + | w.ToggleStashVisible() | |
| 507 | 514 | // The float's own fullscreen, asked while hidden, comes back with it. | |
| 508 | 515 | w.FocusID(2) | |
| 509 | 516 | w.ToggleStashVisible() | |
| @@ -514,3 +521,24 @@ func TestActivateHiddenFloat(t *testing.T) { | |||
| 514 | 521 | t.Fatalf("own fullscreen: focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen) | |
| 515 | 522 | } | |
| 516 | 523 | } | |
| 524 | + | ||
| 525 | + | // A fullscreen game float stays fullscreen when a hidden stashed window is | |
| 526 | + | // activated (xdg-activation, taskbar): the stash shows over it. Ending its | |
| 527 | + | // fullscreen sent the game a 0x0 configure and it quit. | |
| 528 | + | func TestActivateStashKeepsFullscreenFloat(t *testing.T) { | |
| 529 | + | w := workspace() | |
| 530 | + | w.AddWindow(14) | |
| 531 | + | w.ToggleWindowStash() | |
| 532 | + | w.ToggleStashVisible() | |
| 533 | + | w.AddFloating(72, 50, 40) | |
| 534 | + | w.SetFullscreen(72, true) | |
| 535 | + | w.Activate(14) | |
| 536 | + | if id, _ := w.Focused(); id != 14 || !w.stashOverCover() || w.fullscreen != 72 || w.left != 0 { | |
| 537 | + | t.Fatalf("focused %d over %v fullscreen %d left %d", id, w.stashOverCover(), w.fullscreen, w.left) | |
| 538 | + | } | |
| 539 | + | // The game gets the focus back fullscreen. | |
| 540 | + | w.Activate(72) | |
| 541 | + | if id, _ := w.Focused(); id != 72 || w.stashShown() || w.fullscreen != 72 { | |
| 542 | + | t.Fatalf("back: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen) | |
| 543 | + | } | |
| 544 | + | } | |
| @@ -490,8 +490,9 @@ func TestFloatingSceneDimAndConfigure(t *testing.T) { | |||||
| 490 | check(0.3) | 490 | check(0.3) | ||
| 491 | } | 491 | } | ||
| 492 | 492 | ||||
| 493 | - | // Activating a hidden stashed window shows the stash and focuses it; the | 493 | + | // Activating a hidden stashed window shows the stash over the covering |
| 494 | - | // fullscreen window it hid behind leaves fullscreen. | 494 | + | // window and focuses it; the cover stays fullscreen behind and returns |
| 495 | + | // when the stash hides. | |||
| 495 | func TestActivateHiddenFloat(t *testing.T) { | 496 | func TestActivateHiddenFloat(t *testing.T) { | ||
| 496 | w := workspace() | 497 | w := workspace() | ||
| 497 | w.AddWindow(1) | 498 | w.AddWindow(1) | ||
| @@ -501,9 +502,15 @@ func TestActivateHiddenFloat(t *testing.T) { | |||||
| 501 | w.FocusID(1) | 502 | w.FocusID(1) | ||
| 502 | w.SetFullscreen(1, true) | 503 | w.SetFullscreen(1, true) | ||
| 503 | w.Activate(2) | 504 | w.Activate(2) | ||
| 504 | - | if id, _ := w.Focused(); id != 2 || w.stashHidden || w.fullscreen != 0 { | 505 | + | if id, _ := w.Focused(); id != 2 || !w.stashShown() || w.fullscreen != 1 { |
| 505 | - | t.Fatalf("focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen) | 506 | + | t.Fatalf("focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen) |
| 506 | } | 507 | } | ||
| 508 | + | w.ToggleStashVisible() | |||
| 509 | + | if id, _ := w.Focused(); id != 1 || w.stashShown() || w.fullscreen != 1 { | |||
| 510 | + | t.Fatalf("stash hidden: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen) | |||
| 511 | + | } | |||
| 512 | + | w.SetFullscreen(1, false) | |||
| 513 | + | w.ToggleStashVisible() | |||
| 507 | // The float's own fullscreen, asked while hidden, comes back with it. | 514 | // The float's own fullscreen, asked while hidden, comes back with it. | ||
| 508 | w.FocusID(2) | 515 | w.FocusID(2) | ||
| 509 | w.ToggleStashVisible() | 516 | w.ToggleStashVisible() | ||
| @@ -514,3 +521,24 @@ func TestActivateHiddenFloat(t *testing.T) { | |||||
| 514 | t.Fatalf("own fullscreen: focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen) | 521 | t.Fatalf("own fullscreen: focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen) | ||
| 515 | } | 522 | } | ||
| 516 | } | 523 | } | ||
| 524 | + | ||||
| 525 | + | // A fullscreen game float stays fullscreen when a hidden stashed window is | |||
| 526 | + | // activated (xdg-activation, taskbar): the stash shows over it. Ending its | |||
| 527 | + | // fullscreen sent the game a 0x0 configure and it quit. | |||
| 528 | + | func TestActivateStashKeepsFullscreenFloat(t *testing.T) { | |||
| 529 | + | w := workspace() | |||
| 530 | + | w.AddWindow(14) | |||
| 531 | + | w.ToggleWindowStash() | |||
| 532 | + | w.ToggleStashVisible() | |||
| 533 | + | w.AddFloating(72, 50, 40) | |||
| 534 | + | w.SetFullscreen(72, true) | |||
| 535 | + | w.Activate(14) | |||
| 536 | + | if id, _ := w.Focused(); id != 14 || !w.stashOverCover() || w.fullscreen != 72 || w.left != 0 { | |||
| 537 | + | t.Fatalf("focused %d over %v fullscreen %d left %d", id, w.stashOverCover(), w.fullscreen, w.left) | |||
| 538 | + | } | |||
| 539 | + | // The game gets the focus back fullscreen. | |||
| 540 | + | w.Activate(72) | |||
| 541 | + | if id, _ := w.Focused(); id != 72 || w.stashShown() || w.fullscreen != 72 { | |||
| 542 | + | t.Fatalf("back: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen) | |||
| 543 | + | } | |||
| 544 | + | } | |||
M internal/core/fullscreen_left_test.go +4 -4
| @@ -112,9 +112,9 @@ func TestLeftFullscreenRemoved(t *testing.T) { | |||
| 112 | 112 | } | |
| 113 | 113 | } | |
| 114 | 114 | ||
| 115 | - | // Activating a hidden stashed window leaves another window's fullscreen | |
| 116 | - | // with the latch too. | |
| 117 | - | func TestActivateHiddenStashLatches(t *testing.T) { | |
| 115 | + | // Activating a hidden stashed window over a fixed fullscreen tile shows | |
| 116 | + | // the stash over it: the tile stays fullscreen and nothing is latched. | |
| 117 | + | func TestActivateHiddenStashOverFixedCover(t *testing.T) { | |
| 118 | 118 | m := newMonitor("", "") | |
| 119 | 119 | m.SetOutput(300, 200) | |
| 120 | 120 | m.SetOverflow(OverflowFixed) | |
| @@ -127,7 +127,7 @@ func TestActivateHiddenStashLatches(t *testing.T) { | |||
| 127 | 127 | w.FocusID(1) | |
| 128 | 128 | m.ToggleFullscreen() | |
| 129 | 129 | w.Activate(5) | |
| 130 | - | if f, _ := w.Focused(); f != 5 || w.fullscreen != 0 || w.left != 1 { | |
| 130 | + | if f, _ := w.Focused(); f != 5 || w.fullscreen != 1 || w.left != 0 || !w.stashOverCover() { | |
| 131 | 131 | t.Fatalf("focused %d, fullscreen %d, left %d", f, w.fullscreen, w.left) | |
| 132 | 132 | } | |
| 133 | 133 | } | |
| @@ -112,9 +112,9 @@ func TestLeftFullscreenRemoved(t *testing.T) { | |||||
| 112 | } | 112 | } | ||
| 113 | } | 113 | } | ||
| 114 | 114 | ||||
| 115 | - | // Activating a hidden stashed window leaves another window's fullscreen | 115 | + | // Activating a hidden stashed window over a fixed fullscreen tile shows |
| 116 | - | // with the latch too. | 116 | + | // the stash over it: the tile stays fullscreen and nothing is latched. |
| 117 | - | func TestActivateHiddenStashLatches(t *testing.T) { | 117 | + | func TestActivateHiddenStashOverFixedCover(t *testing.T) { |
| 118 | m := newMonitor("", "") | 118 | m := newMonitor("", "") | ||
| 119 | m.SetOutput(300, 200) | 119 | m.SetOutput(300, 200) | ||
| 120 | m.SetOverflow(OverflowFixed) | 120 | m.SetOverflow(OverflowFixed) | ||
| @@ -127,7 +127,7 @@ func TestActivateHiddenStashLatches(t *testing.T) { | |||||
| 127 | w.FocusID(1) | 127 | w.FocusID(1) | ||
| 128 | m.ToggleFullscreen() | 128 | m.ToggleFullscreen() | ||
| 129 | w.Activate(5) | 129 | w.Activate(5) | ||
| 130 | - | if f, _ := w.Focused(); f != 5 || w.fullscreen != 0 || w.left != 1 { | 130 | + | if f, _ := w.Focused(); f != 5 || w.fullscreen != 1 || w.left != 0 || !w.stashOverCover() { |
| 131 | t.Fatalf("focused %d, fullscreen %d, left %d", f, w.fullscreen, w.left) | 131 | t.Fatalf("focused %d, fullscreen %d, left %d", f, w.fullscreen, w.left) | ||
| 132 | } | 132 | } | ||
| 133 | } | 133 | } | ||
M internal/core/layout.go +19 -1
| @@ -1234,8 +1234,18 @@ func (w *Workspace) leaveFullscreen() { | |||
| 1234 | 1234 | } | |
| 1235 | 1235 | ||
| 1236 | 1236 | // Activate focuses a window and makes it visible: it leaves another | |
| 1237 | - | // window's fullscreen, which hides everything else. | |
| 1237 | + | // window's fullscreen, which hides everything else. A stashed window | |
| 1238 | + | // shows over a covering window instead, as ToggleStashVisible does: the | |
| 1239 | + | // cover stays fullscreen behind, so a game never sees its fullscreen end. | |
| 1238 | 1240 | func (w *Workspace) Activate(id WindowID) { | |
| 1241 | + | if i := w.stashIndex(id); i >= 0 && w.coverStays(id) { | |
| 1242 | + | if !w.stashOverCover() { | |
| 1243 | + | w.stashOver, w.stashHidden, w.floatFocus = w.cover(), false, false | |
| 1244 | + | } | |
| 1245 | + | w.stashAt, w.stashFocus = i, true | |
| 1246 | + | w.focusModal(0) | |
| 1247 | + | return | |
| 1248 | + | } | |
| 1239 | 1249 | if i := w.stashIndex(id); i >= 0 && w.stashHidden { | |
| 1240 | 1250 | // A hidden stashed window must be seen: show the stash with it, | |
| 1241 | 1251 | // selected. Another window's fullscreen leaves first, so its own | |
| @@ -1251,6 +1261,14 @@ func (w *Workspace) Activate(id WindowID) { | |||
| 1251 | 1261 | w.focusModal(0) | |
| 1252 | 1262 | } | |
| 1253 | 1263 | ||
| 1264 | + | // coverStays reports whether activating stashed window id keeps the | |
| 1265 | + | // covering window fullscreen under the stash: a cover outside the stash, | |
| 1266 | + | // and no pending fullscreen of id's own that would replace it. | |
| 1267 | + | func (w *Workspace) coverStays(id WindowID) bool { | |
| 1268 | + | c := w.cover() | |
| 1269 | + | return c != 0 && w.stashIndex(c) < 0 && !(w.stashHidden && w.hiddenFullscreen == id) | |
| 1270 | + | } | |
| 1271 | + | ||
| 1254 | 1272 | // SetFullscreen applies a client request. It never moves focus: client | |
| 1255 | 1273 | // requests are automatic events (ADR 011 golden rule). | |
| 1256 | 1274 | func (w *Workspace) SetFullscreen(id WindowID, on bool) { | |
| @@ -1234,8 +1234,18 @@ func (w *Workspace) leaveFullscreen() { | |||||
| 1234 | } | 1234 | } | ||
| 1235 | 1235 | ||||
| 1236 | // Activate focuses a window and makes it visible: it leaves another | 1236 | // Activate focuses a window and makes it visible: it leaves another | ||
| 1237 | - | // window's fullscreen, which hides everything else. | 1237 | + | // window's fullscreen, which hides everything else. A stashed window |
| 1238 | + | // shows over a covering window instead, as ToggleStashVisible does: the | |||
| 1239 | + | // cover stays fullscreen behind, so a game never sees its fullscreen end. | |||
| 1238 | func (w *Workspace) Activate(id WindowID) { | 1240 | func (w *Workspace) Activate(id WindowID) { | ||
| 1241 | + | if i := w.stashIndex(id); i >= 0 && w.coverStays(id) { | |||
| 1242 | + | if !w.stashOverCover() { | |||
| 1243 | + | w.stashOver, w.stashHidden, w.floatFocus = w.cover(), false, false | |||
| 1244 | + | } | |||
| 1245 | + | w.stashAt, w.stashFocus = i, true | |||
| 1246 | + | w.focusModal(0) | |||
| 1247 | + | return | |||
| 1248 | + | } | |||
| 1239 | if i := w.stashIndex(id); i >= 0 && w.stashHidden { | 1249 | if i := w.stashIndex(id); i >= 0 && w.stashHidden { | ||
| 1240 | // A hidden stashed window must be seen: show the stash with it, | 1250 | // A hidden stashed window must be seen: show the stash with it, | ||
| 1241 | // selected. Another window's fullscreen leaves first, so its own | 1251 | // selected. Another window's fullscreen leaves first, so its own | ||
| @@ -1251,6 +1261,14 @@ func (w *Workspace) Activate(id WindowID) { | |||||
| 1251 | w.focusModal(0) | 1261 | w.focusModal(0) | ||
| 1252 | } | 1262 | } | ||
| 1253 | 1263 | ||||
| 1264 | + | // coverStays reports whether activating stashed window id keeps the | |||
| 1265 | + | // covering window fullscreen under the stash: a cover outside the stash, | |||
| 1266 | + | // and no pending fullscreen of id's own that would replace it. | |||
| 1267 | + | func (w *Workspace) coverStays(id WindowID) bool { | |||
| 1268 | + | c := w.cover() | |||
| 1269 | + | return c != 0 && w.stashIndex(c) < 0 && !(w.stashHidden && w.hiddenFullscreen == id) | |||
| 1270 | + | } | |||
| 1271 | + | ||||
| 1254 | // SetFullscreen applies a client request. It never moves focus: client | 1272 | // SetFullscreen applies a client request. It never moves focus: client | ||
| 1255 | // requests are automatic events (ADR 011 golden rule). | 1273 | // requests are automatic events (ADR 011 golden rule). | ||
| 1256 | func (w *Workspace) SetFullscreen(id WindowID, on bool) { | 1274 | func (w *Workspace) SetFullscreen(id WindowID, on bool) { | ||
M internal/core/stash_test.go +5 -5
| @@ -319,9 +319,9 @@ func TestFullscreenStashEdgeGoesToMonitor(t *testing.T) { | |||
| 319 | 319 | } | |
| 320 | 320 | } | |
| 321 | 321 | ||
| 322 | - | // Activating a hidden stashed window over a scroll fullscreen tile ends | |
| 323 | - | // the fullscreen like any exit: the view leaves the fullscreen alignment. | |
| 324 | - | func TestActivateHiddenStashEndsScrollFullscreen(t *testing.T) { | |
| 322 | + | // Activating a hidden stashed window over a scroll fullscreen tile shows | |
| 323 | + | // the stash over it: the tile stays fullscreen and the view stays aligned. | |
| 324 | + | func TestActivateHiddenStashOverScrollFullscreen(t *testing.T) { | |
| 325 | 325 | w := workspace() | |
| 326 | 326 | w.AddWindow(1) | |
| 327 | 327 | w.AddWindow(2) | |
| @@ -331,8 +331,8 @@ func TestActivateHiddenStashEndsScrollFullscreen(t *testing.T) { | |||
| 331 | 331 | w.FocusID(2) | |
| 332 | 332 | w.SetFullscreen(2, true) | |
| 333 | 333 | w.Activate(3) | |
| 334 | - | if id, _ := w.Focused(); id != 3 || w.fullscreen != 0 { | |
| 335 | - | t.Fatalf("focused %d fullscreen %d", id, w.fullscreen) | |
| 334 | + | if id, _ := w.Focused(); id != 3 || w.fullscreen != 2 || !w.stashOverCover() { | |
| 335 | + | t.Fatalf("focused %d fullscreen %d over %v", id, w.fullscreen, w.stashOverCover()) | |
| 336 | 336 | } | |
| 337 | 337 | view := w.View | |
| 338 | 338 | w.scroll() | |
| @@ -319,9 +319,9 @@ func TestFullscreenStashEdgeGoesToMonitor(t *testing.T) { | |||||
| 319 | } | 319 | } | ||
| 320 | } | 320 | } | ||
| 321 | 321 | ||||
| 322 | - | // Activating a hidden stashed window over a scroll fullscreen tile ends | 322 | + | // Activating a hidden stashed window over a scroll fullscreen tile shows |
| 323 | - | // the fullscreen like any exit: the view leaves the fullscreen alignment. | 323 | + | // the stash over it: the tile stays fullscreen and the view stays aligned. |
| 324 | - | func TestActivateHiddenStashEndsScrollFullscreen(t *testing.T) { | 324 | + | func TestActivateHiddenStashOverScrollFullscreen(t *testing.T) { |
| 325 | w := workspace() | 325 | w := workspace() | ||
| 326 | w.AddWindow(1) | 326 | w.AddWindow(1) | ||
| 327 | w.AddWindow(2) | 327 | w.AddWindow(2) | ||
| @@ -331,8 +331,8 @@ func TestActivateHiddenStashEndsScrollFullscreen(t *testing.T) { | |||||
| 331 | w.FocusID(2) | 331 | w.FocusID(2) | ||
| 332 | w.SetFullscreen(2, true) | 332 | w.SetFullscreen(2, true) | ||
| 333 | w.Activate(3) | 333 | w.Activate(3) | ||
| 334 | - | if id, _ := w.Focused(); id != 3 || w.fullscreen != 0 { | 334 | + | if id, _ := w.Focused(); id != 3 || w.fullscreen != 2 || !w.stashOverCover() { |
| 335 | - | t.Fatalf("focused %d fullscreen %d", id, w.fullscreen) | 335 | + | t.Fatalf("focused %d fullscreen %d over %v", id, w.fullscreen, w.stashOverCover()) |
| 336 | } | 336 | } | ||
| 337 | view := w.View | 337 | view := w.View | ||
| 338 | w.scroll() | 338 | w.scroll() | ||