fix(core): activate a stashed window's dialog through its window
A dialog of a hidden stashed window whose own fullscreen waited now brings that window fullscreen, as activating the window does, instead of leaving the cover for plain tiles. Activation tests cover every cover kind for returns and modal close, dialog z-order over the stash, and dialogs that leave the cover. Docs and comments state the exceptions.
- commit
- 264a8c7537cb211ca3559ac801ca6b930c9c34c1
- author
- brice · · 2026-10-13 08:31 UTC
- parent
- e031711
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +113 -34
M docs/config.md +1 -1
| @@ -258,7 +258,7 @@ In a row, the auto columns share the cells the set ones leave; when no cell is l | |||
| 258 | 258 | | `spawn <command>` | Run a program | | |
| 259 | 259 | | `spawn-terminal` | Run `terminal` | | |
| 260 | 260 | | `close-window` | Close the focused window | | |
| 261 | - | | `toggle-fullscreen` | Fullscreen the focused window. An app that asks for fullscreen in its first second (e.g. a Wine launcher that remembered a monitor-sized window) opens in a column instead; later requests are honoured. Fullscreen is exclusive, whether from this bind or the app: nothing is drawn above it but a surface taking the keyboard (a locker). Bars, notifications and other windows wait hidden until it leaves; a window opening meanwhile waits hidden too. The window's own dialogs are the exception: a dialog it opens (a file chooser, a screen-sharing or permission request from the desktop portal), and the dialogs that dialog opens, show over it with the focus, and a focus move goes back to the fullscreen window without leaving fullscreen. Fullscreen stays in place: the window keeps its column. Focusing another window of the workspace (`focus-column-left/right`, `focus-window-up/down`, an activation or the overview) leaves fullscreen, except an activation of a window that can show over it: one of its dialogs, or a stashed window, which shows the stash over it as `toggle-stash-visible` does; the app's own fullscreen requests are then refused until it is focused again. In fixed overflow, an app's own request applies only while it has the focus. A taskbar request brings the window on screen with the focus first | | |
| 261 | + | | `toggle-fullscreen` | Fullscreen the focused window. An app that asks for fullscreen in its first second (e.g. a Wine launcher that remembered a monitor-sized window) opens in a column instead; later requests are honoured. Fullscreen is exclusive, whether from this bind or the app: nothing is drawn above it but a surface taking the keyboard (a locker). Bars, notifications and other windows wait hidden until it leaves; a window opening meanwhile waits hidden too. The window's own dialogs are the exception: a dialog it opens (a file chooser, a screen-sharing or permission request from the desktop portal), and the dialogs that dialog opens, show over it with the focus, and a focus move goes back to the fullscreen window without leaving fullscreen. Fullscreen stays in place: the window keeps its column. Focusing another window of the workspace (`focus-column-left/right`, `focus-window-up/down`, an activation or the overview) leaves fullscreen, and the app's own fullscreen requests are then refused until it is focused again. An activation of a window that can show over it keeps it fullscreen instead: one of its dialogs, or a stashed window or a dialog of one (and their dialogs), which shows the stash over it as `toggle-stash-visible` does, unless that stashed window asked for fullscreen while hidden: it then replaces the fullscreen window. In fixed overflow, an app's own request applies only while it has the focus. A taskbar request brings the window on screen with the focus first | | |
| 262 | 262 | | `toggle-window-stash` | Move the focused tile to the end of the workspace's [stash](#stash), shown and selected; on a stashed window, send it back to its former column, width and maximized state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first | | |
| 263 | 263 | | `toggle-floating` | Make the focused tile a [free floating window](#free-floating-windows) at its current size, centred; on a floating window, send it back to its former place in the columns (a native dialog becomes a new column). Does nothing on a stashed or fullscreen window | | |
| 264 | 264 | | `toggle-stash-visible` | Hide the stash and give the focus back to the tiles, or show it again with the focus on its selected window. Native dialogs stay as they are. Over a fullscreen window, shows the stash on top with the focus; the window stays fullscreen behind (composed, so no direct scanout while the stash shows) and is back when the stash hides or the focus leaves it. `toggle-window-stash` or `toggle-fullscreen` on a stashed window there leaves that fullscreen for it. An activation of a stashed window (an app asking for attention, a taskbar click) shows the stash over the fullscreen window the same way. New windows never join or show over it; dialogs of the fullscreen window or of stashed windows, and their own dialogs, show above it | | |
| @@ -258,7 +258,7 @@ In a row, the auto columns share the cells the set ones leave; when no cell is l | |||||
| 258 | | `spawn <command>` | Run a program | | 258 | | `spawn <command>` | Run a program | | ||
| 259 | | `spawn-terminal` | Run `terminal` | | 259 | | `spawn-terminal` | Run `terminal` | | ||
| 260 | | `close-window` | Close the focused window | | 260 | | `close-window` | Close the focused window | | ||
| 261 | - | | `toggle-fullscreen` | Fullscreen the focused window. An app that asks for fullscreen in its first second (e.g. a Wine launcher that remembered a monitor-sized window) opens in a column instead; later requests are honoured. Fullscreen is exclusive, whether from this bind or the app: nothing is drawn above it but a surface taking the keyboard (a locker). Bars, notifications and other windows wait hidden until it leaves; a window opening meanwhile waits hidden too. The window's own dialogs are the exception: a dialog it opens (a file chooser, a screen-sharing or permission request from the desktop portal), and the dialogs that dialog opens, show over it with the focus, and a focus move goes back to the fullscreen window without leaving fullscreen. Fullscreen stays in place: the window keeps its column. Focusing another window of the workspace (`focus-column-left/right`, `focus-window-up/down`, an activation or the overview) leaves fullscreen, except an activation of a window that can show over it: one of its dialogs, or a stashed window, which shows the stash over it as `toggle-stash-visible` does; the app's own fullscreen requests are then refused until it is focused again. In fixed overflow, an app's own request applies only while it has the focus. A taskbar request brings the window on screen with the focus first | | 261 | + | | `toggle-fullscreen` | Fullscreen the focused window. An app that asks for fullscreen in its first second (e.g. a Wine launcher that remembered a monitor-sized window) opens in a column instead; later requests are honoured. Fullscreen is exclusive, whether from this bind or the app: nothing is drawn above it but a surface taking the keyboard (a locker). Bars, notifications and other windows wait hidden until it leaves; a window opening meanwhile waits hidden too. The window's own dialogs are the exception: a dialog it opens (a file chooser, a screen-sharing or permission request from the desktop portal), and the dialogs that dialog opens, show over it with the focus, and a focus move goes back to the fullscreen window without leaving fullscreen. Fullscreen stays in place: the window keeps its column. Focusing another window of the workspace (`focus-column-left/right`, `focus-window-up/down`, an activation or the overview) leaves fullscreen, and the app's own fullscreen requests are then refused until it is focused again. An activation of a window that can show over it keeps it fullscreen instead: one of its dialogs, or a stashed window or a dialog of one (and their dialogs), which shows the stash over it as `toggle-stash-visible` does, unless that stashed window asked for fullscreen while hidden: it then replaces the fullscreen window. In fixed overflow, an app's own request applies only while it has the focus. A taskbar request brings the window on screen with the focus first | |
| 262 | | `toggle-window-stash` | Move the focused tile to the end of the workspace's [stash](#stash), shown and selected; on a stashed window, send it back to its former column, width and maximized state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first | | 262 | | `toggle-window-stash` | Move the focused tile to the end of the workspace's [stash](#stash), shown and selected; on a stashed window, send it back to its former column, width and maximized state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first | | ||
| 263 | | `toggle-floating` | Make the focused tile a [free floating window](#free-floating-windows) at its current size, centred; on a floating window, send it back to its former place in the columns (a native dialog becomes a new column). Does nothing on a stashed or fullscreen window | | 263 | | `toggle-floating` | Make the focused tile a [free floating window](#free-floating-windows) at its current size, centred; on a floating window, send it back to its former place in the columns (a native dialog becomes a new column). Does nothing on a stashed or fullscreen window | | ||
| 264 | | `toggle-stash-visible` | Hide the stash and give the focus back to the tiles, or show it again with the focus on its selected window. Native dialogs stay as they are. Over a fullscreen window, shows the stash on top with the focus; the window stays fullscreen behind (composed, so no direct scanout while the stash shows) and is back when the stash hides or the focus leaves it. `toggle-window-stash` or `toggle-fullscreen` on a stashed window there leaves that fullscreen for it. An activation of a stashed window (an app asking for attention, a taskbar click) shows the stash over the fullscreen window the same way. New windows never join or show over it; dialogs of the fullscreen window or of stashed windows, and their own dialogs, show above it | | 264 | | `toggle-stash-visible` | Hide the stash and give the focus back to the tiles, or show it again with the focus on its selected window. Native dialogs stay as they are. Over a fullscreen window, shows the stash on top with the focus; the window stays fullscreen behind (composed, so no direct scanout while the stash shows) and is back when the stash hides or the focus leaves it. `toggle-window-stash` or `toggle-fullscreen` on a stashed window there leaves that fullscreen for it. An activation of a stashed window (an app asking for attention, a taskbar click) shows the stash over the fullscreen window the same way. New windows never join or show over it; dialogs of the fullscreen window or of stashed windows, and their own dialogs, show above it | | ||
M internal/core/layout.go +12 -2
| @@ -393,7 +393,8 @@ func (w *Workspace) focusModal(from WindowID) { | |||
| 393 | 393 | if c := w.cover(); c != 0 && c != d { | |
| 394 | 394 | if i := w.floatIndex(d); i >= 0 && !w.overCover(w.Floats[i]) { | |
| 395 | 395 | // Hidden under another window's fullscreen: leave it, as | |
| 396 | - | // Activate does for a tile, so the dialog shows. | |
| 396 | + | // Activate does for a window not shown over the cover, so | |
| 397 | + | // the dialog shows. | |
| 397 | 398 | w.leaveFullscreen() | |
| 398 | 399 | } | |
| 399 | 400 | } | |
| @@ -1266,13 +1267,22 @@ func (w *Workspace) leaveFullscreen() { | |||
| 1266 | 1267 | // window's fullscreen, which hides everything else. Over a covering | |
| 1267 | 1268 | // window, a dialog drawn over it, a stashed window or a dialog of one | |
| 1268 | 1269 | // shows over it instead, as ToggleStashVisible does: the cover stays | |
| 1269 | - | // fullscreen behind, so a game never sees its fullscreen end. | |
| 1270 | + | // fullscreen behind, so a game never sees its fullscreen end. A stashed | |
| 1271 | + | // window whose own fullscreen waited while hidden replaces the cover | |
| 1272 | + | // (coverStays); a dialog of a stashed window activates that window | |
| 1273 | + | // first. | |
| 1270 | 1274 | func (w *Workspace) Activate(id WindowID) { | |
| 1271 | 1275 | if c := w.cover(); c != 0 && id != c { | |
| 1272 | 1276 | owner := id | |
| 1273 | 1277 | if f := w.floatIndex(id); f >= 0 { | |
| 1274 | 1278 | owner = w.dialogOwner(w.Floats[f], c) | |
| 1275 | 1279 | } | |
| 1280 | + | if owner != id && owner != c && w.stashIndex(owner) >= 0 && !w.coverStays(owner) { | |
| 1281 | + | w.Activate(owner) | |
| 1282 | + | w.FocusID(id) | |
| 1283 | + | w.focusModal(0) | |
| 1284 | + | return | |
| 1285 | + | } | |
| 1276 | 1286 | if i := w.stashIndex(owner); i >= 0 && w.coverStays(owner) { | |
| 1277 | 1287 | if !w.stashOverCover() { | |
| 1278 | 1288 | w.showStashOver() | |
| @@ -393,7 +393,8 @@ func (w *Workspace) focusModal(from WindowID) { | |||||
| 393 | if c := w.cover(); c != 0 && c != d { | 393 | if c := w.cover(); c != 0 && c != d { | ||
| 394 | if i := w.floatIndex(d); i >= 0 && !w.overCover(w.Floats[i]) { | 394 | if i := w.floatIndex(d); i >= 0 && !w.overCover(w.Floats[i]) { | ||
| 395 | // Hidden under another window's fullscreen: leave it, as | 395 | // Hidden under another window's fullscreen: leave it, as | ||
| 396 | - | // Activate does for a tile, so the dialog shows. | 396 | + | // Activate does for a window not shown over the cover, so |
| 397 | + | // the dialog shows. | |||
| 397 | w.leaveFullscreen() | 398 | w.leaveFullscreen() | ||
| 398 | } | 399 | } | ||
| 399 | } | 400 | } | ||
| @@ -1266,13 +1267,22 @@ func (w *Workspace) leaveFullscreen() { | |||||
| 1266 | // window's fullscreen, which hides everything else. Over a covering | 1267 | // window's fullscreen, which hides everything else. Over a covering | ||
| 1267 | // window, a dialog drawn over it, a stashed window or a dialog of one | 1268 | // window, a dialog drawn over it, a stashed window or a dialog of one | ||
| 1268 | // shows over it instead, as ToggleStashVisible does: the cover stays | 1269 | // shows over it instead, as ToggleStashVisible does: the cover stays | ||
| 1269 | - | // fullscreen behind, so a game never sees its fullscreen end. | 1270 | + | // fullscreen behind, so a game never sees its fullscreen end. A stashed |
| 1271 | + | // window whose own fullscreen waited while hidden replaces the cover | |||
| 1272 | + | // (coverStays); a dialog of a stashed window activates that window | |||
| 1273 | + | // first. | |||
| 1270 | func (w *Workspace) Activate(id WindowID) { | 1274 | func (w *Workspace) Activate(id WindowID) { | ||
| 1271 | if c := w.cover(); c != 0 && id != c { | 1275 | if c := w.cover(); c != 0 && id != c { | ||
| 1272 | owner := id | 1276 | owner := id | ||
| 1273 | if f := w.floatIndex(id); f >= 0 { | 1277 | if f := w.floatIndex(id); f >= 0 { | ||
| 1274 | owner = w.dialogOwner(w.Floats[f], c) | 1278 | owner = w.dialogOwner(w.Floats[f], c) | ||
| 1275 | } | 1279 | } | ||
| 1280 | + | if owner != id && owner != c && w.stashIndex(owner) >= 0 && !w.coverStays(owner) { | |||
| 1281 | + | w.Activate(owner) | |||
| 1282 | + | w.FocusID(id) | |||
| 1283 | + | w.focusModal(0) | |||
| 1284 | + | return | |||
| 1285 | + | } | |||
| 1276 | if i := w.stashIndex(owner); i >= 0 && w.coverStays(owner) { | 1286 | if i := w.stashIndex(owner); i >= 0 && w.coverStays(owner) { | ||
| 1277 | if !w.stashOverCover() { | 1287 | if !w.stashOverCover() { | ||
| 1278 | w.showStashOver() | 1288 | w.showStashOver() | ||
M internal/core/stash_cover_test.go +100 -31
| @@ -228,6 +228,12 @@ func TestActivateOverCover(t *testing.T) { | |||
| 228 | 228 | t.Run(cv.name+"/"+tc.name, func(t *testing.T) { | |
| 229 | 229 | _, w, cover := stashCoverCore(t, cv.overflow, cv.float) | |
| 230 | 230 | id := tc.setup(w, cover) | |
| 231 | + | // A dialog of the hidden stash waits hidden under the cover. | |
| 232 | + | if i := slices.IndexFunc(w.Floats, func(f Float) bool { return f.over == 2 }); i >= 0 && !w.stashOverCover() { | |
| 233 | + | if p := previewOf(t, w.Layout(), w.Floats[i].ID); !p.Hidden { | |
| 234 | + | t.Fatalf("dialog of the hidden stash shown before activation: %+v", p) | |
| 235 | + | } | |
| 236 | + | } | |
| 231 | 237 | view := w.View | |
| 232 | 238 | w.Activate(id) | |
| 233 | 239 | if got, _ := w.Focused(); got != tc.focused || w.cover() != cover || w.left != 0 || w.stashOverCover() != tc.over { | |
| @@ -237,9 +243,13 @@ func TestActivateOverCover(t *testing.T) { | |||
| 237 | 243 | if p := previewOf(t, ps, cover); p.Hidden || !p.Fullscreen { | |
| 238 | 244 | t.Fatalf("cover %+v", p) | |
| 239 | 245 | } | |
| 240 | - | if f, c := slices.IndexFunc(ps, isID(tc.focused)), slices.IndexFunc(ps, isID(cover)); f < c || ps[f].Hidden { | |
| 246 | + | f := slices.IndexFunc(ps, isID(tc.focused)) | |
| 247 | + | if f < slices.IndexFunc(ps, isID(cover)) || ps[f].Hidden { | |
| 241 | 248 | t.Fatalf("focused window not over the cover: %+v", ps) | |
| 242 | 249 | } | |
| 250 | + | if tc.over && f < slices.IndexFunc(ps, isID(2)) { | |
| 251 | + | t.Fatalf("focused window under the stash: %+v", ps) | |
| 252 | + | } | |
| 243 | 253 | if !cv.float && w.View != view { | |
| 244 | 254 | t.Fatalf("view %d, was %d", w.View, view) | |
| 245 | 255 | } | |
| @@ -249,40 +259,99 @@ func TestActivateOverCover(t *testing.T) { | |||
| 249 | 259 | } | |
| 250 | 260 | ||
| 251 | 261 | // After an activation shows the stash over the cover, hiding the stash | |
| 252 | - | // or activating the cover returns to it, and a dialog of a stashed | |
| 253 | - | // window mapping meanwhile shows with the focus. | |
| 262 | + | // or activating the cover returns to it; a dialog of a stashed window, | |
| 263 | + | // modal or mapping meanwhile, shows with the focus and closing it | |
| 264 | + | // returns to the stash over the cover. | |
| 254 | 265 | func TestActivateOverCoverReturns(t *testing.T) { | |
| 255 | - | _, w, cover := stashCoverCore(t, OverflowFixed, true) | |
| 256 | - | w.Activate(2) | |
| 257 | - | w.ToggleStashVisible() | |
| 258 | - | if id, _ := w.Focused(); id != cover || w.stashShown() { | |
| 259 | - | t.Fatalf("hide: focused %d shown %v", id, w.stashShown()) | |
| 260 | - | } | |
| 261 | - | w.Activate(2) | |
| 262 | - | w.Activate(cover) | |
| 263 | - | if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover { | |
| 264 | - | t.Fatalf("activate cover: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen) | |
| 265 | - | } | |
| 266 | - | w.Activate(2) | |
| 267 | - | w.AddDialog(9, 2, 20, 10) | |
| 268 | - | w.AddDialog(8, 9, 10, 5) | |
| 269 | - | if id, _ := w.Focused(); id != 8 || previewOf(t, w.Layout(), 8).Hidden { | |
| 270 | - | t.Fatalf("mapped chain: focused %d", id) | |
| 271 | - | } | |
| 272 | - | // Closing the dialogs returns to the stash over the cover. | |
| 273 | - | w.RemoveWindow(8) | |
| 274 | - | w.RemoveWindow(9) | |
| 275 | - | if id, _ := w.Focused(); id != 2 || !w.stashOverCover() { | |
| 276 | - | t.Fatalf("closed: focused %d over %v", id, w.stashOverCover()) | |
| 277 | - | } | |
| 278 | - | // The cover closes: the stash stays shown and focused, the override | |
| 279 | - | // ends with it. | |
| 280 | - | w.RemoveWindow(cover) | |
| 281 | - | if id, _ := w.Focused(); id != 2 || w.stashOver != 0 || !w.stashShown() { | |
| 282 | - | t.Fatalf("cover closed: focused %d over %d shown %v", id, w.stashOver, w.stashShown()) | |
| 266 | + | for _, cv := range []struct { | |
| 267 | + | name string | |
| 268 | + | overflow Overflow | |
| 269 | + | float bool | |
| 270 | + | }{ | |
| 271 | + | {"fixed tile", OverflowFixed, false}, | |
| 272 | + | {"scroll tile", OverflowScroll, false}, | |
| 273 | + | {"fixed float", OverflowFixed, true}, | |
| 274 | + | {"scroll float", OverflowScroll, true}, | |
| 275 | + | } { | |
| 276 | + | t.Run(cv.name, func(t *testing.T) { | |
| 277 | + | _, w, cover := stashCoverCore(t, cv.overflow, cv.float) | |
| 278 | + | w.Activate(2) | |
| 279 | + | w.ToggleStashVisible() | |
| 280 | + | if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover { | |
| 281 | + | t.Fatalf("hide: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen) | |
| 282 | + | } | |
| 283 | + | w.Activate(2) | |
| 284 | + | w.Activate(cover) | |
| 285 | + | if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover { | |
| 286 | + | t.Fatalf("activate cover: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen) | |
| 287 | + | } | |
| 288 | + | w.AddDialog(7, 2, 20, 10) | |
| 289 | + | w.SetModal(7, true) | |
| 290 | + | w.FocusID(cover) | |
| 291 | + | w.Activate(2) | |
| 292 | + | w.RemoveWindow(7) | |
| 293 | + | if id, _ := w.Focused(); id != 2 || !w.stashOverCover() { | |
| 294 | + | t.Fatalf("modal closed: focused %d over %v", id, w.stashOverCover()) | |
| 295 | + | } | |
| 296 | + | w.AddDialog(9, 2, 20, 10) | |
| 297 | + | w.AddDialog(8, 9, 10, 5) | |
| 298 | + | if id, _ := w.Focused(); id != 8 || previewOf(t, w.Layout(), 8).Hidden { | |
| 299 | + | t.Fatalf("mapped chain: focused %d", id) | |
| 300 | + | } | |
| 301 | + | w.RemoveWindow(8) | |
| 302 | + | w.RemoveWindow(9) | |
| 303 | + | if id, _ := w.Focused(); id != 2 || !w.stashOverCover() { | |
| 304 | + | t.Fatalf("closed: focused %d over %v", id, w.stashOverCover()) | |
| 305 | + | } | |
| 306 | + | // The cover closes: the stash stays shown and focused, the | |
| 307 | + | // override ends with it. | |
| 308 | + | w.RemoveWindow(cover) | |
| 309 | + | if id, _ := w.Focused(); id != 2 || w.stashOver != 0 || !w.stashShown() { | |
| 310 | + | t.Fatalf("cover closed: focused %d over %d shown %v", id, w.stashOver, w.stashShown()) | |
| 311 | + | } | |
| 312 | + | }) | |
| 283 | 313 | } | |
| 284 | 314 | } | |
| 285 | 315 | ||
| 316 | + | // Activating a dialog that cannot show over the cover leaves fullscreen | |
| 317 | + | // as any other window does; a dialog of a stashed window whose own | |
| 318 | + | // fullscreen waited while hidden brings that window fullscreen, as | |
| 319 | + | // activating the window itself does. | |
| 320 | + | func TestActivateDialogLeavesCover(t *testing.T) { | |
| 321 | + | t.Run("dialog of a plain float", func(t *testing.T) { | |
| 322 | + | _, w, cover := stashCoverCore(t, OverflowFixed, false) | |
| 323 | + | w.AddFloating(5, 20, 10) | |
| 324 | + | w.AddDialog(9, 5, 10, 5) | |
| 325 | + | w.FocusID(cover) | |
| 326 | + | w.Activate(9) | |
| 327 | + | if id, _ := w.Focused(); id != 9 || w.fullscreen != 0 || w.left != cover { | |
| 328 | + | t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left) | |
| 329 | + | } | |
| 330 | + | }) | |
| 331 | + | t.Run("dialog of another tile", func(t *testing.T) { | |
| 332 | + | _, w, cover := stashCoverCore(t, OverflowFixed, true) | |
| 333 | + | w.AddDialog(9, 1, 10, 5) | |
| 334 | + | w.FocusID(cover) | |
| 335 | + | w.Activate(9) | |
| 336 | + | if id, _ := w.Focused(); id != 9 || w.fullscreen != 0 || w.left != cover { | |
| 337 | + | t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left) | |
| 338 | + | } | |
| 339 | + | }) | |
| 340 | + | t.Run("dialog of a stashed window with a pending fullscreen", func(t *testing.T) { | |
| 341 | + | _, w, cover := stashCoverCore(t, OverflowFixed, true) | |
| 342 | + | w.SetFullscreen(2, true) | |
| 343 | + | w.AddDialog(9, 2, 10, 5) | |
| 344 | + | w.FocusID(cover) | |
| 345 | + | w.Activate(9) | |
| 346 | + | if id, _ := w.Focused(); id != 9 || w.fullscreen != 2 || w.stashHidden || w.left != cover { | |
| 347 | + | t.Fatalf("focused %d fullscreen %d hidden %v left %d", id, w.fullscreen, w.stashHidden, w.left) | |
| 348 | + | } | |
| 349 | + | if p := previewOf(t, w.Layout(), 9); p.Hidden { | |
| 350 | + | t.Fatalf("dialog hidden: %+v", p) | |
| 351 | + | } | |
| 352 | + | }) | |
| 353 | + | } | |
| 354 | + | ||
| 286 | 355 | // A dialog parent loop under a cover ends: neither dialog shows over it. | |
| 287 | 356 | func TestOverCoverDialogLoop(t *testing.T) { | |
| 288 | 357 | _, w, _ := stashCoverCore(t, OverflowFixed, true) | |
| @@ -228,6 +228,12 @@ func TestActivateOverCover(t *testing.T) { | |||||
| 228 | t.Run(cv.name+"/"+tc.name, func(t *testing.T) { | 228 | t.Run(cv.name+"/"+tc.name, func(t *testing.T) { | ||
| 229 | _, w, cover := stashCoverCore(t, cv.overflow, cv.float) | 229 | _, w, cover := stashCoverCore(t, cv.overflow, cv.float) | ||
| 230 | id := tc.setup(w, cover) | 230 | id := tc.setup(w, cover) | ||
| 231 | + | // A dialog of the hidden stash waits hidden under the cover. | |||
| 232 | + | if i := slices.IndexFunc(w.Floats, func(f Float) bool { return f.over == 2 }); i >= 0 && !w.stashOverCover() { | |||
| 233 | + | if p := previewOf(t, w.Layout(), w.Floats[i].ID); !p.Hidden { | |||
| 234 | + | t.Fatalf("dialog of the hidden stash shown before activation: %+v", p) | |||
| 235 | + | } | |||
| 236 | + | } | |||
| 231 | view := w.View | 237 | view := w.View | ||
| 232 | w.Activate(id) | 238 | w.Activate(id) | ||
| 233 | if got, _ := w.Focused(); got != tc.focused || w.cover() != cover || w.left != 0 || w.stashOverCover() != tc.over { | 239 | if got, _ := w.Focused(); got != tc.focused || w.cover() != cover || w.left != 0 || w.stashOverCover() != tc.over { | ||
| @@ -237,9 +243,13 @@ func TestActivateOverCover(t *testing.T) { | |||||
| 237 | if p := previewOf(t, ps, cover); p.Hidden || !p.Fullscreen { | 243 | if p := previewOf(t, ps, cover); p.Hidden || !p.Fullscreen { | ||
| 238 | t.Fatalf("cover %+v", p) | 244 | t.Fatalf("cover %+v", p) | ||
| 239 | } | 245 | } | ||
| 240 | - | if f, c := slices.IndexFunc(ps, isID(tc.focused)), slices.IndexFunc(ps, isID(cover)); f < c || ps[f].Hidden { | 246 | + | f := slices.IndexFunc(ps, isID(tc.focused)) |
| 247 | + | if f < slices.IndexFunc(ps, isID(cover)) || ps[f].Hidden { | |||
| 241 | t.Fatalf("focused window not over the cover: %+v", ps) | 248 | t.Fatalf("focused window not over the cover: %+v", ps) | ||
| 242 | } | 249 | } | ||
| 250 | + | if tc.over && f < slices.IndexFunc(ps, isID(2)) { | |||
| 251 | + | t.Fatalf("focused window under the stash: %+v", ps) | |||
| 252 | + | } | |||
| 243 | if !cv.float && w.View != view { | 253 | if !cv.float && w.View != view { | ||
| 244 | t.Fatalf("view %d, was %d", w.View, view) | 254 | t.Fatalf("view %d, was %d", w.View, view) | ||
| 245 | } | 255 | } | ||
| @@ -249,40 +259,99 @@ func TestActivateOverCover(t *testing.T) { | |||||
| 249 | } | 259 | } | ||
| 250 | 260 | ||||
| 251 | // After an activation shows the stash over the cover, hiding the stash | 261 | // After an activation shows the stash over the cover, hiding the stash | ||
| 252 | - | // or activating the cover returns to it, and a dialog of a stashed | 262 | + | // or activating the cover returns to it; a dialog of a stashed window, |
| 253 | - | // window mapping meanwhile shows with the focus. | 263 | + | // modal or mapping meanwhile, shows with the focus and closing it |
| 264 | + | // returns to the stash over the cover. | |||
| 254 | func TestActivateOverCoverReturns(t *testing.T) { | 265 | func TestActivateOverCoverReturns(t *testing.T) { | ||
| 255 | - | _, w, cover := stashCoverCore(t, OverflowFixed, true) | 266 | + | for _, cv := range []struct { |
| 256 | - | w.Activate(2) | 267 | + | name string |
| 257 | - | w.ToggleStashVisible() | 268 | + | overflow Overflow |
| 258 | - | if id, _ := w.Focused(); id != cover || w.stashShown() { | 269 | + | float bool |
| 259 | - | t.Fatalf("hide: focused %d shown %v", id, w.stashShown()) | 270 | + | }{ |
| 260 | - | } | 271 | + | {"fixed tile", OverflowFixed, false}, |
| 261 | - | w.Activate(2) | 272 | + | {"scroll tile", OverflowScroll, false}, |
| 262 | - | w.Activate(cover) | 273 | + | {"fixed float", OverflowFixed, true}, |
| 263 | - | if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover { | 274 | + | {"scroll float", OverflowScroll, true}, |
| 264 | - | t.Fatalf("activate cover: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen) | 275 | + | } { |
| 265 | - | } | 276 | + | t.Run(cv.name, func(t *testing.T) { |
| 266 | - | w.Activate(2) | 277 | + | _, w, cover := stashCoverCore(t, cv.overflow, cv.float) |
| 267 | - | w.AddDialog(9, 2, 20, 10) | 278 | + | w.Activate(2) |
| 268 | - | w.AddDialog(8, 9, 10, 5) | 279 | + | w.ToggleStashVisible() |
| 269 | - | if id, _ := w.Focused(); id != 8 || previewOf(t, w.Layout(), 8).Hidden { | 280 | + | if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover { |
| 270 | - | t.Fatalf("mapped chain: focused %d", id) | 281 | + | t.Fatalf("hide: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen) |
| 271 | - | } | 282 | + | } |
| 272 | - | // Closing the dialogs returns to the stash over the cover. | 283 | + | w.Activate(2) |
| 273 | - | w.RemoveWindow(8) | 284 | + | w.Activate(cover) |
| 274 | - | w.RemoveWindow(9) | 285 | + | if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover { |
| 275 | - | if id, _ := w.Focused(); id != 2 || !w.stashOverCover() { | 286 | + | t.Fatalf("activate cover: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen) |
| 276 | - | t.Fatalf("closed: focused %d over %v", id, w.stashOverCover()) | 287 | + | } |
| 277 | - | } | 288 | + | w.AddDialog(7, 2, 20, 10) |
| 278 | - | // The cover closes: the stash stays shown and focused, the override | 289 | + | w.SetModal(7, true) |
| 279 | - | // ends with it. | 290 | + | w.FocusID(cover) |
| 280 | - | w.RemoveWindow(cover) | 291 | + | w.Activate(2) |
| 281 | - | if id, _ := w.Focused(); id != 2 || w.stashOver != 0 || !w.stashShown() { | 292 | + | w.RemoveWindow(7) |
| 282 | - | t.Fatalf("cover closed: focused %d over %d shown %v", id, w.stashOver, w.stashShown()) | 293 | + | if id, _ := w.Focused(); id != 2 || !w.stashOverCover() { |
| 294 | + | t.Fatalf("modal closed: focused %d over %v", id, w.stashOverCover()) | |||
| 295 | + | } | |||
| 296 | + | w.AddDialog(9, 2, 20, 10) | |||
| 297 | + | w.AddDialog(8, 9, 10, 5) | |||
| 298 | + | if id, _ := w.Focused(); id != 8 || previewOf(t, w.Layout(), 8).Hidden { | |||
| 299 | + | t.Fatalf("mapped chain: focused %d", id) | |||
| 300 | + | } | |||
| 301 | + | w.RemoveWindow(8) | |||
| 302 | + | w.RemoveWindow(9) | |||
| 303 | + | if id, _ := w.Focused(); id != 2 || !w.stashOverCover() { | |||
| 304 | + | t.Fatalf("closed: focused %d over %v", id, w.stashOverCover()) | |||
| 305 | + | } | |||
| 306 | + | // The cover closes: the stash stays shown and focused, the | |||
| 307 | + | // override ends with it. | |||
| 308 | + | w.RemoveWindow(cover) | |||
| 309 | + | if id, _ := w.Focused(); id != 2 || w.stashOver != 0 || !w.stashShown() { | |||
| 310 | + | t.Fatalf("cover closed: focused %d over %d shown %v", id, w.stashOver, w.stashShown()) | |||
| 311 | + | } | |||
| 312 | + | }) | |||
| 283 | } | 313 | } | ||
| 284 | } | 314 | } | ||
| 285 | 315 | ||||
| 316 | + | // Activating a dialog that cannot show over the cover leaves fullscreen | |||
| 317 | + | // as any other window does; a dialog of a stashed window whose own | |||
| 318 | + | // fullscreen waited while hidden brings that window fullscreen, as | |||
| 319 | + | // activating the window itself does. | |||
| 320 | + | func TestActivateDialogLeavesCover(t *testing.T) { | |||
| 321 | + | t.Run("dialog of a plain float", func(t *testing.T) { | |||
| 322 | + | _, w, cover := stashCoverCore(t, OverflowFixed, false) | |||
| 323 | + | w.AddFloating(5, 20, 10) | |||
| 324 | + | w.AddDialog(9, 5, 10, 5) | |||
| 325 | + | w.FocusID(cover) | |||
| 326 | + | w.Activate(9) | |||
| 327 | + | if id, _ := w.Focused(); id != 9 || w.fullscreen != 0 || w.left != cover { | |||
| 328 | + | t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left) | |||
| 329 | + | } | |||
| 330 | + | }) | |||
| 331 | + | t.Run("dialog of another tile", func(t *testing.T) { | |||
| 332 | + | _, w, cover := stashCoverCore(t, OverflowFixed, true) | |||
| 333 | + | w.AddDialog(9, 1, 10, 5) | |||
| 334 | + | w.FocusID(cover) | |||
| 335 | + | w.Activate(9) | |||
| 336 | + | if id, _ := w.Focused(); id != 9 || w.fullscreen != 0 || w.left != cover { | |||
| 337 | + | t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left) | |||
| 338 | + | } | |||
| 339 | + | }) | |||
| 340 | + | t.Run("dialog of a stashed window with a pending fullscreen", func(t *testing.T) { | |||
| 341 | + | _, w, cover := stashCoverCore(t, OverflowFixed, true) | |||
| 342 | + | w.SetFullscreen(2, true) | |||
| 343 | + | w.AddDialog(9, 2, 10, 5) | |||
| 344 | + | w.FocusID(cover) | |||
| 345 | + | w.Activate(9) | |||
| 346 | + | if id, _ := w.Focused(); id != 9 || w.fullscreen != 2 || w.stashHidden || w.left != cover { | |||
| 347 | + | t.Fatalf("focused %d fullscreen %d hidden %v left %d", id, w.fullscreen, w.stashHidden, w.left) | |||
| 348 | + | } | |||
| 349 | + | if p := previewOf(t, w.Layout(), 9); p.Hidden { | |||
| 350 | + | t.Fatalf("dialog hidden: %+v", p) | |||
| 351 | + | } | |||
| 352 | + | }) | |||
| 353 | + | } | |||
| 354 | + | ||||
| 286 | // A dialog parent loop under a cover ends: neither dialog shows over it. | 355 | // A dialog parent loop under a cover ends: neither dialog shows over it. | ||
| 287 | func TestOverCoverDialogLoop(t *testing.T) { | 356 | func TestOverCoverDialogLoop(t *testing.T) { | ||
| 288 | _, w, _ := stashCoverCore(t, OverflowFixed, true) | 357 | _, w, _ := stashCoverCore(t, OverflowFixed, true) | ||