detached ee436b8

fix(core): show stashed windows' dialogs over the cover with the stash

A modal dialog of a stashed window activated over a fullscreen cover made the cover leave fullscreen. While the stash shows over the cover, dialogs of stashed windows show over it too (overCover), so a game stays fullscreen.
commit
ee436b889db264bf2b7629de714b3c05c7b7eba7
author
brice · · 2026-10-13 04:11 UTC
parent
1a34129
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +66 -15
M internal/core/floating_test.go +46 -0
@@ -543,6 +543,52 @@ func TestActivateStashKeepsFullscreenFloat(t *testing.T) {
543543 }
544544 }
545545
546+// A modal dialog of the activated stashed window shows over the cover,
547+// focused, and the game stays fullscreen; closing it returns to the stash
548+// over the cover.
549+func TestActivateStashModalKeepsFullscreen(t *testing.T) {
550+ w := workspace()
551+ w.AddWindow(14)
552+ w.ToggleWindowStash()
553+ w.ToggleStashVisible()
554+ w.AddDialog(9, 14, 20, 10)
555+ w.SetModal(9, true)
556+ w.AddFloating(72, 50, 40)
557+ w.SetFullscreen(72, true)
558+ if p := placementOf(w.Layout(), 9); !p.Hidden {
559+ t.Fatalf("modal shown over the game before activation: %+v", p)
560+ }
561+ w.Activate(14)
562+ if id, _ := w.Focused(); id != 9 || w.fullscreen != 72 || w.left != 0 || !w.stashOverCover() {
563+ t.Fatalf("focused %d fullscreen %d left %d over %v", id, w.fullscreen, w.left, w.stashOverCover())
564+ }
565+ layout := w.Layout()
566+ if p := placementOf(layout, 9); p.Hidden || p.Rect.W == 0 {
567+ t.Fatalf("modal hidden: %+v", p)
568+ }
569+ if p := placementOf(layout, 72); !p.Fullscreen {
570+ t.Fatalf("game left fullscreen: %+v", p)
571+ }
572+ if placementIndex(layout, 9) < placementIndex(layout, 14) || placementIndex(layout, 14) < placementIndex(layout, 72) {
573+ t.Fatalf("order: game %d stash %d modal %d", placementIndex(layout, 72), placementIndex(layout, 14), placementIndex(layout, 9))
574+ }
575+ w.RemoveWindow(9)
576+ if id, _ := w.Focused(); id != 14 || w.fullscreen != 72 || !w.stashOverCover() {
577+ t.Fatalf("closed: focused %d fullscreen %d over %v", id, w.fullscreen, w.stashOverCover())
578+ }
579+}
580+
581+func placementOf(ps []Placement, id WindowID) Placement {
582+ if i := placementIndex(ps, id); i >= 0 {
583+ return ps[i]
584+ }
585+ return Placement{}
586+}
587+
588+func placementIndex(ps []Placement, id WindowID) int {
589+ return slices.IndexFunc(ps, func(p Placement) bool { return p.ID == id })
590+}
591+
546592 // With the stash already over the cover and a dialog of the cover
547593 // focused, activating a stashed window focuses it.
548594 func TestActivateStashOverCoverFromCoverDialog(t *testing.T) {
@@ -543,6 +543,52 @@ func TestActivateStashKeepsFullscreenFloat(t *testing.T) {
543 }543 }
544 }544 }
545 545
546+// A modal dialog of the activated stashed window shows over the cover,
547+// focused, and the game stays fullscreen; closing it returns to the stash
548+// over the cover.
549+func TestActivateStashModalKeepsFullscreen(t *testing.T) {
550+ w := workspace()
551+ w.AddWindow(14)
552+ w.ToggleWindowStash()
553+ w.ToggleStashVisible()
554+ w.AddDialog(9, 14, 20, 10)
555+ w.SetModal(9, true)
556+ w.AddFloating(72, 50, 40)
557+ w.SetFullscreen(72, true)
558+ if p := placementOf(w.Layout(), 9); !p.Hidden {
559+ t.Fatalf("modal shown over the game before activation: %+v", p)
560+ }
561+ w.Activate(14)
562+ if id, _ := w.Focused(); id != 9 || w.fullscreen != 72 || w.left != 0 || !w.stashOverCover() {
563+ t.Fatalf("focused %d fullscreen %d left %d over %v", id, w.fullscreen, w.left, w.stashOverCover())
564+ }
565+ layout := w.Layout()
566+ if p := placementOf(layout, 9); p.Hidden || p.Rect.W == 0 {
567+ t.Fatalf("modal hidden: %+v", p)
568+ }
569+ if p := placementOf(layout, 72); !p.Fullscreen {
570+ t.Fatalf("game left fullscreen: %+v", p)
571+ }
572+ if placementIndex(layout, 9) < placementIndex(layout, 14) || placementIndex(layout, 14) < placementIndex(layout, 72) {
573+ t.Fatalf("order: game %d stash %d modal %d", placementIndex(layout, 72), placementIndex(layout, 14), placementIndex(layout, 9))
574+ }
575+ w.RemoveWindow(9)
576+ if id, _ := w.Focused(); id != 14 || w.fullscreen != 72 || !w.stashOverCover() {
577+ t.Fatalf("closed: focused %d fullscreen %d over %v", id, w.fullscreen, w.stashOverCover())
578+ }
579+}
580+
581+func placementOf(ps []Placement, id WindowID) Placement {
582+ if i := placementIndex(ps, id); i >= 0 {
583+ return ps[i]
584+ }
585+ return Placement{}
586+}
587+
588+func placementIndex(ps []Placement, id WindowID) int {
589+ return slices.IndexFunc(ps, func(p Placement) bool { return p.ID == id })
590+}
591+
546 // With the stash already over the cover and a dialog of the cover592 // With the stash already over the cover and a dialog of the cover
547 // focused, activating a stashed window focuses it.593 // focused, activating a stashed window focuses it.
548 func TestActivateStashOverCoverFromCoverDialog(t *testing.T) {594 func TestActivateStashOverCoverFromCoverDialog(t *testing.T) {
M internal/core/layout.go +20 -15
@@ -390,7 +390,7 @@ func (w *Workspace) focusModal(from WindowID) {
390390 return
391391 }
392392 if c := w.cover(); c != 0 && c != d {
393- if i := w.floatIndex(d); i >= 0 && !w.Floats[i].coverDialog(c) {
393+ if i := w.floatIndex(d); i >= 0 && !w.overCover(w.Floats[i]) {
394394 // Hidden under another window's fullscreen: leave it, as
395395 // Activate does, so the dialog shows.
396396 w.leaveFullscreen()
@@ -399,22 +399,26 @@ func (w *Workspace) focusModal(from WindowID) {
399399 w.FocusID(d)
400400 }
401401
402-// coverDialog reports whether f is a dialog of the covering fullscreen
403-// window cover: the one float drawn over it.
404-func (f Float) coverDialog(cover WindowID) bool {
405- return cover != 0 && f.over == cover && f.ID != cover
402+// overCover reports whether float f is drawn over the covering fullscreen
403+// window: a dialog of the cover, or of a stashed window while the stash
404+// shows over the cover (a "Save as…" of the app the user picked there).
405+func (w *Workspace) overCover(f Float) bool {
406+ c := w.cover()
407+ if c == 0 || f.ID == c || f.over == 0 {
408+ return false
409+ }
410+ return f.over == c || w.stashOverCover() && w.stashIndex(f.over) >= 0
406411 }
407412
408413 // coverDialog is the topmost dialog shown over the covering fullscreen
409-// window when the floats have the focus, else 0. The floats it hides cannot
410-// take the focus.
414+// window (overCover) when the floats have the focus, else 0. The floats it
415+// hides cannot take the focus.
411416 func (w *Workspace) coverDialog() WindowID {
412417 if !w.floatFocus {
413418 return 0
414419 }
415- cover := w.cover()
416420 for i := len(w.Floats) - 1; i >= 0; i-- {
417- if f := w.Floats[i]; !f.below && f.coverDialog(cover) {
421+ if f := w.Floats[i]; !f.below && w.overCover(f) {
418422 return f.ID
419423 }
420424 }
@@ -638,9 +642,9 @@ func (w *Workspace) FocusID(id WindowID) bool {
638642 f := w.Floats[i]
639643 f.below = false
640644 w.Floats = append(slices.Delete(w.Floats, i, i+1), f)
641- if !f.coverDialog(w.cover()) {
642- // A dialog of the cover shows over the stash; leaving it
643- // returns there.
645+ if !w.overCover(f) {
646+ // A dialog shown over the cover keeps the stash over it;
647+ // leaving it returns there.
644648 w.dropStashOver()
645649 }
646650 w.floatFocus = true
@@ -1788,9 +1792,10 @@ func (w *Workspace) appendLayout(dst []Placement) []Placement {
17881792 if order != group {
17891793 continue
17901794 }
1791- // A covering fullscreen window hides every float but its own
1792- // dialogs: the others show again when it leaves fullscreen.
1793- p := Placement{ID: f.ID, Rect: w.floatRect(f), Floating: true, Below: below, Focused: f.ID == focusedID, Inset: ports.SideAll, Hidden: cover != 0 && f.ID != cover && !f.coverDialog(cover)}
1795+ // A covering fullscreen window hides every float but the
1796+ // dialogs over it (overCover): the others show again when it
1797+ // leaves fullscreen.
1798+ p := Placement{ID: f.ID, Rect: w.floatRect(f), Floating: true, Below: below, Focused: f.ID == focusedID, Inset: ports.SideAll, Hidden: cover != 0 && f.ID != cover && !w.overCover(f)}
17941799 if p.Hidden {
17951800 p.Rect = Rect{}
17961801 result = append(result, p)
@@ -390,7 +390,7 @@ func (w *Workspace) focusModal(from WindowID) {
390 return390 return
391 }391 }
392 if c := w.cover(); c != 0 && c != d {392 if c := w.cover(); c != 0 && c != d {
393- if i := w.floatIndex(d); i >= 0 && !w.Floats[i].coverDialog(c) {393+ if i := w.floatIndex(d); i >= 0 && !w.overCover(w.Floats[i]) {
394 // Hidden under another window's fullscreen: leave it, as394 // Hidden under another window's fullscreen: leave it, as
395 // Activate does, so the dialog shows.395 // Activate does, so the dialog shows.
396 w.leaveFullscreen()396 w.leaveFullscreen()
@@ -399,22 +399,26 @@ func (w *Workspace) focusModal(from WindowID) {
399 w.FocusID(d)399 w.FocusID(d)
400 }400 }
401 401
402-// coverDialog reports whether f is a dialog of the covering fullscreen402+// overCover reports whether float f is drawn over the covering fullscreen
403-// window cover: the one float drawn over it.403+// window: a dialog of the cover, or of a stashed window while the stash
404-func (f Float) coverDialog(cover WindowID) bool {404+// shows over the cover (a "Save as…" of the app the user picked there).
405- return cover != 0 && f.over == cover && f.ID != cover405+func (w *Workspace) overCover(f Float) bool {
406+ c := w.cover()
407+ if c == 0 || f.ID == c || f.over == 0 {
408+ return false
409+ }
410+ return f.over == c || w.stashOverCover() && w.stashIndex(f.over) >= 0
406 }411 }
407 412
408 // coverDialog is the topmost dialog shown over the covering fullscreen413 // coverDialog is the topmost dialog shown over the covering fullscreen
409-// window when the floats have the focus, else 0. The floats it hides cannot414+// window (overCover) when the floats have the focus, else 0. The floats it
410-// take the focus.415+// hides cannot take the focus.
411 func (w *Workspace) coverDialog() WindowID {416 func (w *Workspace) coverDialog() WindowID {
412 if !w.floatFocus {417 if !w.floatFocus {
413 return 0418 return 0
414 }419 }
415- cover := w.cover()
416 for i := len(w.Floats) - 1; i >= 0; i-- {420 for i := len(w.Floats) - 1; i >= 0; i-- {
417- if f := w.Floats[i]; !f.below && f.coverDialog(cover) {421+ if f := w.Floats[i]; !f.below && w.overCover(f) {
418 return f.ID422 return f.ID
419 }423 }
420 }424 }
@@ -638,9 +642,9 @@ func (w *Workspace) FocusID(id WindowID) bool {
638 f := w.Floats[i]642 f := w.Floats[i]
639 f.below = false643 f.below = false
640 w.Floats = append(slices.Delete(w.Floats, i, i+1), f)644 w.Floats = append(slices.Delete(w.Floats, i, i+1), f)
641- if !f.coverDialog(w.cover()) {645+ if !w.overCover(f) {
642- // A dialog of the cover shows over the stash; leaving it646+ // A dialog shown over the cover keeps the stash over it;
643- // returns there.647+ // leaving it returns there.
644 w.dropStashOver()648 w.dropStashOver()
645 }649 }
646 w.floatFocus = true650 w.floatFocus = true
@@ -1788,9 +1792,10 @@ func (w *Workspace) appendLayout(dst []Placement) []Placement {
1788 if order != group {1792 if order != group {
1789 continue1793 continue
1790 }1794 }
1791- // A covering fullscreen window hides every float but its own1795+ // A covering fullscreen window hides every float but the
1792- // dialogs: the others show again when it leaves fullscreen.1796+ // dialogs over it (overCover): the others show again when it
1793- p := Placement{ID: f.ID, Rect: w.floatRect(f), Floating: true, Below: below, Focused: f.ID == focusedID, Inset: ports.SideAll, Hidden: cover != 0 && f.ID != cover && !f.coverDialog(cover)}1797+ // leaves fullscreen.
1798+ p := Placement{ID: f.ID, Rect: w.floatRect(f), Floating: true, Below: below, Focused: f.ID == focusedID, Inset: ports.SideAll, Hidden: cover != 0 && f.ID != cover && !w.overCover(f)}
1794 if p.Hidden {1799 if p.Hidden {
1795 p.Rect = Rect{}1800 p.Rect = Rect{}
1796 result = append(result, p)1801 result = append(result, p)

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 26

Tags 10

↑↓ moveenter openesc close