detached 699102b

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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) {}
267267
268268 func (t *toplevelRequests) Activate(*wlr.ZwlrForeignToplevelHandleV1, *wayland.Seat) {
269269 if w := t.window(); w != nil {
270+ t.s.log.Info().Uint64("id", uint64(w.id)).Msg("foreign toplevel activation")
270271 t.s.emit(ports.WindowActivate{ID: w.id})
271272 }
272273 }
@@ -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) {
490490 check(0.3)
491491 }
492492
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.
495496 func TestActivateHiddenFloat(t *testing.T) {
496497 w := workspace()
497498 w.AddWindow(1)
@@ -501,9 +502,15 @@ func TestActivateHiddenFloat(t *testing.T) {
501502 w.FocusID(1)
502503 w.SetFullscreen(1, true)
503504 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)
506507 }
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()
507514 // The float's own fullscreen, asked while hidden, comes back with it.
508515 w.FocusID(2)
509516 w.ToggleStashVisible()
@@ -514,3 +521,24 @@ func TestActivateHiddenFloat(t *testing.T) {
514521 t.Fatalf("own fullscreen: focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen)
515522 }
516523 }
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; the493+// 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) {
112112 }
113113 }
114114
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) {
118118 m := newMonitor("", "")
119119 m.SetOutput(300, 200)
120120 m.SetOverflow(OverflowFixed)
@@ -127,7 +127,7 @@ func TestActivateHiddenStashLatches(t *testing.T) {
127127 w.FocusID(1)
128128 m.ToggleFullscreen()
129129 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() {
131131 t.Fatalf("focused %d, fullscreen %d, left %d", f, w.fullscreen, w.left)
132132 }
133133 }
@@ -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 fullscreen115+// 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() {
12341234 }
12351235
12361236 // 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.
12381240 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+ }
12391249 if i := w.stashIndex(id); i >= 0 && w.stashHidden {
12401250 // A hidden stashed window must be seen: show the stash with it,
12411251 // selected. Another window's fullscreen leaves first, so its own
@@ -1251,6 +1261,14 @@ func (w *Workspace) Activate(id WindowID) {
12511261 w.focusModal(0)
12521262 }
12531263
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+
12541272 // SetFullscreen applies a client request. It never moves focus: client
12551273 // requests are automatic events (ADR 011 golden rule).
12561274 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 another1236 // 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 own1251 // 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: client1272 // 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) {
319319 }
320320 }
321321
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) {
325325 w := workspace()
326326 w.AddWindow(1)
327327 w.AddWindow(2)
@@ -331,8 +331,8 @@ func TestActivateHiddenStashEndsScrollFullscreen(t *testing.T) {
331331 w.FocusID(2)
332332 w.SetFullscreen(2, true)
333333 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())
336336 }
337337 view := w.View
338338 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 ends322+// 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.View337 view := w.View
338 w.scroll()338 w.scroll()

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close