fix(core): keep the cover fullscreen when a dialog over it is activated
Activating a dialog drawn over a fullscreen cover, or a dialog of a stashed window, still ended the cover's fullscreen. Activate now resolves the dialog's owner (dialogOwner) and shows it over the cover. showStashOver shares the stash-over setup with toggle-stash-visible. Activation tests move to a table over every cover kind, with a core test on the game's configure. Docs describe activations and dialogs over a fullscreen window.
- commit
- e03171101862f87d5865458959174ea26ac6fe48
- author
- brice · · 2026-10-13 08:25 UTC
- parent
- 92f3c09
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +229 -202
M docs/config.md +3 -3
| @@ -258,10 +258,10 @@ 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) shows 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; 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, 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 | | |
| 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 | - | | `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. New windows never join or show over it; the fullscreen window's 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 | | |
| 265 | 265 | | `toggle-overview` | Open the [overview](#overview), or close it on the selected window | | |
| 266 | 266 | | `switch-column-next` | Switch to the column used before this one on the workspace. Hold Cmd and press again to go further back; after a short hold a strip of cards shows the columns, most recent first, and releasing Cmd focuses the selected one. Escape cancels; clicking a card picks it, and a click elsewhere cancels and is not passed to windows. A click before the cards show cancels and acts as usual. A maximized column hands its maximization to the target. A workspace capture session pauses while the strip shows, as in the overview | | |
| 267 | 267 | | `switch-column-prev` | The same, in the opposite direction | | |
| @@ -290,7 +290,7 @@ Each workspace has a stash: a horizontal strip of windows set aside with `toggle | |||
| 290 | 290 | ||
| 291 | 291 | While the stash has the focus, `focus-column-left/right` and the three-finger swipe move through it, sliding the view, and stop at its ends; `focus-window-up/down` do nothing. Hide it with `toggle-stash-visible`, or with a four-finger swipe down or up, to get back to the tiles; a swipe down shows it again. Scripts see each stashed window's place, and whether the stash is hidden, in the [state file](desktop.md#state-for-scripts). | |
| 292 | 292 | ||
| 293 | - | Native floating windows are not in the stash: they stay centred in the usable output area. A window-sized float that fills that area (within two border widths plus two logical pixels per axis) stays below the columns when you focus a tile; `focus-window-up` at the top of a column raises it again. It does not hide bars or pin focus. Small dialogs and file pickers always stay above the columns; shrinking a float promotes it above them. Real fullscreen remains exclusive, except for the fullscreen window's own dialogs. A dialog belongs to a window through `xdg_toplevel.set_parent`, or through `zxdg_importer_v2` when another program opens it, as the desktop portal does. A dialog the app marks modal through `xdg_dialog_v1` (GTK4 does for "Save as…") blocks its window: clicking it, activating it, picking it in the overview or moving the focus onto it within its workspace focuses the dialog instead, and so do nested modal dialogs. A focus move away from the dialog reaches its window once, so the next move can go past it. | |
| 293 | + | Native floating windows are not in the stash: they stay centred in the usable output area. A window-sized float that fills that area (within two border widths plus two logical pixels per axis) stays below the columns when you focus a tile; `focus-window-up` at the top of a column raises it again. It does not hide bars or pin focus. Small dialogs and file pickers always stay above the columns; shrinking a float promotes it above them. Real fullscreen remains exclusive, except for the dialogs shown over it: the fullscreen window's own, those of stashed windows while the stash shows over it, and their own dialogs. A dialog belongs to a window through `xdg_toplevel.set_parent`, or through `zxdg_importer_v2` when another program opens it, as the desktop portal does. A dialog the app marks modal through `xdg_dialog_v1` (GTK4 does for "Save as…") blocks its window: clicking it, activating it, picking it in the overview or moving the focus onto it within its workspace focuses the dialog instead, and so do nested modal dialogs. A focus move away from the dialog reaches its window once, so the next move can go past it. | |
| 294 | 294 | ||
| 295 | 295 | ### Free floating windows | |
| 296 | 296 | ||
| @@ -258,10 +258,10 @@ 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) shows 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; 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, 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 | |
| 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. New windows never join or show over it; the fullscreen window's 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 | |
| 265 | | `toggle-overview` | Open the [overview](#overview), or close it on the selected window | | 265 | | `toggle-overview` | Open the [overview](#overview), or close it on the selected window | | ||
| 266 | | `switch-column-next` | Switch to the column used before this one on the workspace. Hold Cmd and press again to go further back; after a short hold a strip of cards shows the columns, most recent first, and releasing Cmd focuses the selected one. Escape cancels; clicking a card picks it, and a click elsewhere cancels and is not passed to windows. A click before the cards show cancels and acts as usual. A maximized column hands its maximization to the target. A workspace capture session pauses while the strip shows, as in the overview | | 266 | | `switch-column-next` | Switch to the column used before this one on the workspace. Hold Cmd and press again to go further back; after a short hold a strip of cards shows the columns, most recent first, and releasing Cmd focuses the selected one. Escape cancels; clicking a card picks it, and a click elsewhere cancels and is not passed to windows. A click before the cards show cancels and acts as usual. A maximized column hands its maximization to the target. A workspace capture session pauses while the strip shows, as in the overview | | ||
| 267 | | `switch-column-prev` | The same, in the opposite direction | | 267 | | `switch-column-prev` | The same, in the opposite direction | | ||
| @@ -290,7 +290,7 @@ Each workspace has a stash: a horizontal strip of windows set aside with `toggle | |||||
| 290 | 290 | ||||
| 291 | While the stash has the focus, `focus-column-left/right` and the three-finger swipe move through it, sliding the view, and stop at its ends; `focus-window-up/down` do nothing. Hide it with `toggle-stash-visible`, or with a four-finger swipe down or up, to get back to the tiles; a swipe down shows it again. Scripts see each stashed window's place, and whether the stash is hidden, in the [state file](desktop.md#state-for-scripts). | 291 | While the stash has the focus, `focus-column-left/right` and the three-finger swipe move through it, sliding the view, and stop at its ends; `focus-window-up/down` do nothing. Hide it with `toggle-stash-visible`, or with a four-finger swipe down or up, to get back to the tiles; a swipe down shows it again. Scripts see each stashed window's place, and whether the stash is hidden, in the [state file](desktop.md#state-for-scripts). | ||
| 292 | 292 | ||||
| 293 | - | Native floating windows are not in the stash: they stay centred in the usable output area. A window-sized float that fills that area (within two border widths plus two logical pixels per axis) stays below the columns when you focus a tile; `focus-window-up` at the top of a column raises it again. It does not hide bars or pin focus. Small dialogs and file pickers always stay above the columns; shrinking a float promotes it above them. Real fullscreen remains exclusive, except for the fullscreen window's own dialogs. A dialog belongs to a window through `xdg_toplevel.set_parent`, or through `zxdg_importer_v2` when another program opens it, as the desktop portal does. A dialog the app marks modal through `xdg_dialog_v1` (GTK4 does for "Save as…") blocks its window: clicking it, activating it, picking it in the overview or moving the focus onto it within its workspace focuses the dialog instead, and so do nested modal dialogs. A focus move away from the dialog reaches its window once, so the next move can go past it. | 293 | + | Native floating windows are not in the stash: they stay centred in the usable output area. A window-sized float that fills that area (within two border widths plus two logical pixels per axis) stays below the columns when you focus a tile; `focus-window-up` at the top of a column raises it again. It does not hide bars or pin focus. Small dialogs and file pickers always stay above the columns; shrinking a float promotes it above them. Real fullscreen remains exclusive, except for the dialogs shown over it: the fullscreen window's own, those of stashed windows while the stash shows over it, and their own dialogs. A dialog belongs to a window through `xdg_toplevel.set_parent`, or through `zxdg_importer_v2` when another program opens it, as the desktop portal does. A dialog the app marks modal through `xdg_dialog_v1` (GTK4 does for "Save as…") blocks its window: clicking it, activating it, picking it in the overview or moving the focus onto it within its workspace focuses the dialog instead, and so do nested modal dialogs. A focus move away from the dialog reaches its window once, so the next move can go past it. |
| 294 | 294 | ||||
| 295 | ### Free floating windows | 295 | ### Free floating windows | ||
| 296 | 296 | ||||
M internal/core/floating_test.go +2 -158
| @@ -490,9 +490,8 @@ func TestFloatingSceneDimAndConfigure(t *testing.T) { | |||
| 490 | 490 | check(0.3) | |
| 491 | 491 | } | |
| 492 | 492 | ||
| 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. | |
| 493 | + | // A stashed float's own fullscreen, asked while the stash was hidden, | |
| 494 | + | // comes back with it when it is activated: it replaces the cover. | |
| 496 | 495 | func TestActivateHiddenFloat(t *testing.T) { | |
| 497 | 496 | w := workspace() | |
| 498 | 497 | w.AddWindow(1) | |
| @@ -500,20 +499,6 @@ func TestActivateHiddenFloat(t *testing.T) { | |||
| 500 | 499 | w.ToggleWindowStash() | |
| 501 | 500 | w.ToggleStashVisible() | |
| 502 | 501 | w.FocusID(1) | |
| 503 | - | w.SetFullscreen(1, true) | |
| 504 | - | w.Activate(2) | |
| 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) | |
| 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() | |
| 514 | - | // The float's own fullscreen, asked while hidden, comes back with it. | |
| 515 | - | w.FocusID(2) | |
| 516 | - | w.ToggleStashVisible() | |
| 517 | 502 | w.SetFullscreen(2, true) | |
| 518 | 503 | w.SetFullscreen(1, true) | |
| 519 | 504 | w.Activate(2) | |
| @@ -521,144 +506,3 @@ func TestActivateHiddenFloat(t *testing.T) { | |||
| 521 | 506 | t.Fatalf("own fullscreen: focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen) | |
| 522 | 507 | } | |
| 523 | 508 | } | |
| 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 | - | } | |
| 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 | - | // A dialog of a dialog (an "Overwrite?" over a "Save as…") shows over | |
| 582 | - | // the cover like its parent: modal chains of a stashed window or of the | |
| 583 | - | // cover keep the game fullscreen, and a new one takes the focus. | |
| 584 | - | func TestDialogChainOverCover(t *testing.T) { | |
| 585 | - | t.Run("stashed modal chain", func(t *testing.T) { | |
| 586 | - | w := workspace() | |
| 587 | - | w.AddWindow(14) | |
| 588 | - | w.ToggleWindowStash() | |
| 589 | - | w.ToggleStashVisible() | |
| 590 | - | w.AddDialog(9, 14, 20, 10) | |
| 591 | - | w.SetModal(9, true) | |
| 592 | - | w.AddDialog(8, 9, 10, 5) | |
| 593 | - | w.SetModal(8, true) | |
| 594 | - | w.AddFloating(72, 50, 40) | |
| 595 | - | w.SetFullscreen(72, true) | |
| 596 | - | w.Activate(14) | |
| 597 | - | if id, _ := w.Focused(); id != 8 || w.fullscreen != 72 || w.left != 0 { | |
| 598 | - | t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left) | |
| 599 | - | } | |
| 600 | - | if p := placementOf(w.Layout(), 8); p.Hidden { | |
| 601 | - | t.Fatalf("confirm hidden: %+v", p) | |
| 602 | - | } | |
| 603 | - | }) | |
| 604 | - | t.Run("stashed dialog maps a dialog", func(t *testing.T) { | |
| 605 | - | w := workspace() | |
| 606 | - | w.AddWindow(14) | |
| 607 | - | w.ToggleWindowStash() | |
| 608 | - | w.ToggleStashVisible() | |
| 609 | - | w.AddFloating(72, 50, 40) | |
| 610 | - | w.SetFullscreen(72, true) | |
| 611 | - | w.Activate(14) | |
| 612 | - | w.AddDialog(9, 14, 20, 10) | |
| 613 | - | w.AddDialog(8, 9, 10, 5) | |
| 614 | - | if id, _ := w.Focused(); id != 8 || w.fullscreen != 72 { | |
| 615 | - | t.Fatalf("focused %d fullscreen %d", id, w.fullscreen) | |
| 616 | - | } | |
| 617 | - | if p := placementOf(w.Layout(), 8); p.Hidden { | |
| 618 | - | t.Fatalf("confirm hidden: %+v", p) | |
| 619 | - | } | |
| 620 | - | }) | |
| 621 | - | t.Run("cover modal chain", func(t *testing.T) { | |
| 622 | - | w := workspace() | |
| 623 | - | w.AddFloating(72, 50, 40) | |
| 624 | - | w.AddDialog(9, 72, 20, 10) | |
| 625 | - | w.SetModal(9, true) | |
| 626 | - | w.AddDialog(8, 9, 10, 5) | |
| 627 | - | w.SetModal(8, true) | |
| 628 | - | w.SetFullscreen(72, true) | |
| 629 | - | w.AddWindow(1) | |
| 630 | - | w.Activate(72) | |
| 631 | - | if id, _ := w.Focused(); id != 8 || w.fullscreen != 72 || w.left != 0 { | |
| 632 | - | t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left) | |
| 633 | - | } | |
| 634 | - | }) | |
| 635 | - | } | |
| 636 | - | ||
| 637 | - | func placementOf(ps []Placement, id WindowID) Placement { | |
| 638 | - | if i := placementIndex(ps, id); i >= 0 { | |
| 639 | - | return ps[i] | |
| 640 | - | } | |
| 641 | - | return Placement{} | |
| 642 | - | } | |
| 643 | - | ||
| 644 | - | func placementIndex(ps []Placement, id WindowID) int { | |
| 645 | - | return slices.IndexFunc(ps, func(p Placement) bool { return p.ID == id }) | |
| 646 | - | } | |
| 647 | - | ||
| 648 | - | // With the stash already over the cover and a dialog of the cover | |
| 649 | - | // focused, activating a stashed window focuses it. | |
| 650 | - | func TestActivateStashOverCoverFromCoverDialog(t *testing.T) { | |
| 651 | - | w := workspace() | |
| 652 | - | w.AddWindow(14) | |
| 653 | - | w.ToggleWindowStash() | |
| 654 | - | w.ToggleStashVisible() | |
| 655 | - | w.AddWindow(1) | |
| 656 | - | w.SetFullscreen(1, true) | |
| 657 | - | w.ToggleStashVisible() | |
| 658 | - | w.AddDialog(9, 1, 20, 10) | |
| 659 | - | w.FocusID(9) | |
| 660 | - | w.Activate(14) | |
| 661 | - | if id, _ := w.Focused(); id != 14 || !w.stashOverCover() || w.fullscreen != 1 { | |
| 662 | - | t.Fatalf("focused %d over %v fullscreen %d", id, w.stashOverCover(), w.fullscreen) | |
| 663 | - | } | |
| 664 | - | } | |
| @@ -490,9 +490,8 @@ 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 over the covering | 493 | + | // A stashed float's own fullscreen, asked while the stash was hidden, |
| 494 | - | // window and focuses it; the cover stays fullscreen behind and returns | 494 | + | // comes back with it when it is activated: it replaces the cover. |
| 495 | - | // when the stash hides. | |||
| 496 | func TestActivateHiddenFloat(t *testing.T) { | 495 | func TestActivateHiddenFloat(t *testing.T) { | ||
| 497 | w := workspace() | 496 | w := workspace() | ||
| 498 | w.AddWindow(1) | 497 | w.AddWindow(1) | ||
| @@ -500,20 +499,6 @@ func TestActivateHiddenFloat(t *testing.T) { | |||||
| 500 | w.ToggleWindowStash() | 499 | w.ToggleWindowStash() | ||
| 501 | w.ToggleStashVisible() | 500 | w.ToggleStashVisible() | ||
| 502 | w.FocusID(1) | 501 | w.FocusID(1) | ||
| 503 | - | w.SetFullscreen(1, true) | |||
| 504 | - | w.Activate(2) | |||
| 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) | |||
| 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() | |||
| 514 | - | // The float's own fullscreen, asked while hidden, comes back with it. | |||
| 515 | - | w.FocusID(2) | |||
| 516 | - | w.ToggleStashVisible() | |||
| 517 | w.SetFullscreen(2, true) | 502 | w.SetFullscreen(2, true) | ||
| 518 | w.SetFullscreen(1, true) | 503 | w.SetFullscreen(1, true) | ||
| 519 | w.Activate(2) | 504 | w.Activate(2) | ||
| @@ -521,144 +506,3 @@ func TestActivateHiddenFloat(t *testing.T) { | |||||
| 521 | t.Fatalf("own fullscreen: focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen) | 506 | t.Fatalf("own fullscreen: focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen) | ||
| 522 | } | 507 | } | ||
| 523 | } | 508 | } | ||
| 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 | - | } | |||
| 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 | - | // A dialog of a dialog (an "Overwrite?" over a "Save as…") shows over | |||
| 582 | - | // the cover like its parent: modal chains of a stashed window or of the | |||
| 583 | - | // cover keep the game fullscreen, and a new one takes the focus. | |||
| 584 | - | func TestDialogChainOverCover(t *testing.T) { | |||
| 585 | - | t.Run("stashed modal chain", func(t *testing.T) { | |||
| 586 | - | w := workspace() | |||
| 587 | - | w.AddWindow(14) | |||
| 588 | - | w.ToggleWindowStash() | |||
| 589 | - | w.ToggleStashVisible() | |||
| 590 | - | w.AddDialog(9, 14, 20, 10) | |||
| 591 | - | w.SetModal(9, true) | |||
| 592 | - | w.AddDialog(8, 9, 10, 5) | |||
| 593 | - | w.SetModal(8, true) | |||
| 594 | - | w.AddFloating(72, 50, 40) | |||
| 595 | - | w.SetFullscreen(72, true) | |||
| 596 | - | w.Activate(14) | |||
| 597 | - | if id, _ := w.Focused(); id != 8 || w.fullscreen != 72 || w.left != 0 { | |||
| 598 | - | t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left) | |||
| 599 | - | } | |||
| 600 | - | if p := placementOf(w.Layout(), 8); p.Hidden { | |||
| 601 | - | t.Fatalf("confirm hidden: %+v", p) | |||
| 602 | - | } | |||
| 603 | - | }) | |||
| 604 | - | t.Run("stashed dialog maps a dialog", func(t *testing.T) { | |||
| 605 | - | w := workspace() | |||
| 606 | - | w.AddWindow(14) | |||
| 607 | - | w.ToggleWindowStash() | |||
| 608 | - | w.ToggleStashVisible() | |||
| 609 | - | w.AddFloating(72, 50, 40) | |||
| 610 | - | w.SetFullscreen(72, true) | |||
| 611 | - | w.Activate(14) | |||
| 612 | - | w.AddDialog(9, 14, 20, 10) | |||
| 613 | - | w.AddDialog(8, 9, 10, 5) | |||
| 614 | - | if id, _ := w.Focused(); id != 8 || w.fullscreen != 72 { | |||
| 615 | - | t.Fatalf("focused %d fullscreen %d", id, w.fullscreen) | |||
| 616 | - | } | |||
| 617 | - | if p := placementOf(w.Layout(), 8); p.Hidden { | |||
| 618 | - | t.Fatalf("confirm hidden: %+v", p) | |||
| 619 | - | } | |||
| 620 | - | }) | |||
| 621 | - | t.Run("cover modal chain", func(t *testing.T) { | |||
| 622 | - | w := workspace() | |||
| 623 | - | w.AddFloating(72, 50, 40) | |||
| 624 | - | w.AddDialog(9, 72, 20, 10) | |||
| 625 | - | w.SetModal(9, true) | |||
| 626 | - | w.AddDialog(8, 9, 10, 5) | |||
| 627 | - | w.SetModal(8, true) | |||
| 628 | - | w.SetFullscreen(72, true) | |||
| 629 | - | w.AddWindow(1) | |||
| 630 | - | w.Activate(72) | |||
| 631 | - | if id, _ := w.Focused(); id != 8 || w.fullscreen != 72 || w.left != 0 { | |||
| 632 | - | t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left) | |||
| 633 | - | } | |||
| 634 | - | }) | |||
| 635 | - | } | |||
| 636 | - | ||||
| 637 | - | func placementOf(ps []Placement, id WindowID) Placement { | |||
| 638 | - | if i := placementIndex(ps, id); i >= 0 { | |||
| 639 | - | return ps[i] | |||
| 640 | - | } | |||
| 641 | - | return Placement{} | |||
| 642 | - | } | |||
| 643 | - | ||||
| 644 | - | func placementIndex(ps []Placement, id WindowID) int { | |||
| 645 | - | return slices.IndexFunc(ps, func(p Placement) bool { return p.ID == id }) | |||
| 646 | - | } | |||
| 647 | - | ||||
| 648 | - | // With the stash already over the cover and a dialog of the cover | |||
| 649 | - | // focused, activating a stashed window focuses it. | |||
| 650 | - | func TestActivateStashOverCoverFromCoverDialog(t *testing.T) { | |||
| 651 | - | w := workspace() | |||
| 652 | - | w.AddWindow(14) | |||
| 653 | - | w.ToggleWindowStash() | |||
| 654 | - | w.ToggleStashVisible() | |||
| 655 | - | w.AddWindow(1) | |||
| 656 | - | w.SetFullscreen(1, true) | |||
| 657 | - | w.ToggleStashVisible() | |||
| 658 | - | w.AddDialog(9, 1, 20, 10) | |||
| 659 | - | w.FocusID(9) | |||
| 660 | - | w.Activate(14) | |||
| 661 | - | if id, _ := w.Focused(); id != 14 || !w.stashOverCover() || w.fullscreen != 1 { | |||
| 662 | - | t.Fatalf("focused %d over %v fullscreen %d", id, w.stashOverCover(), w.fullscreen) | |||
| 663 | - | } | |||
| 664 | - | } | |||
M internal/core/fullscreen_left_test.go +0 -20
| @@ -111,23 +111,3 @@ func TestLeftFullscreenRemoved(t *testing.T) { | |||
| 111 | 111 | t.Fatalf("left %d", w.left) | |
| 112 | 112 | } | |
| 113 | 113 | } | |
| 114 | - | ||
| 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 | - | m := newMonitor("", "") | |
| 119 | - | m.SetOutput(300, 200) | |
| 120 | - | m.SetOverflow(OverflowFixed) | |
| 121 | - | w := m.Current() | |
| 122 | - | w.AddWindow(1) | |
| 123 | - | w.AddWindow(5) | |
| 124 | - | w.FocusID(5) | |
| 125 | - | w.ToggleWindowStash() | |
| 126 | - | w.ToggleStashVisible() | |
| 127 | - | w.FocusID(1) | |
| 128 | - | m.ToggleFullscreen() | |
| 129 | - | w.Activate(5) | |
| 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) | |
| 132 | - | } | |
| 133 | - | } | |
| @@ -111,23 +111,3 @@ func TestLeftFullscreenRemoved(t *testing.T) { | |||||
| 111 | t.Fatalf("left %d", w.left) | 111 | t.Fatalf("left %d", w.left) | ||
| 112 | } | 112 | } | ||
| 113 | } | 113 | } | ||
| 114 | - | ||||
| 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 | - | m := newMonitor("", "") | |||
| 119 | - | m.SetOutput(300, 200) | |||
| 120 | - | m.SetOverflow(OverflowFixed) | |||
| 121 | - | w := m.Current() | |||
| 122 | - | w.AddWindow(1) | |||
| 123 | - | w.AddWindow(5) | |||
| 124 | - | w.FocusID(5) | |||
| 125 | - | w.ToggleWindowStash() | |||
| 126 | - | w.ToggleStashVisible() | |||
| 127 | - | w.FocusID(1) | |||
| 128 | - | m.ToggleFullscreen() | |||
| 129 | - | w.Activate(5) | |||
| 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) | |||
| 132 | - | } | |||
| 133 | - | } | |||
M internal/core/layout.go +37 -17
| @@ -146,7 +146,8 @@ type Workspace struct { | |||
| 146 | 146 | // is hidden, restored when shown. | |
| 147 | 147 | hiddenFullscreen WindowID | |
| 148 | 148 | // stashOver is the covering fullscreen window the stash shows over, a | |
| 149 | - | // user override (ToggleStashVisible); 0 without. Bound to that window, | |
| 149 | + | // user override (ToggleStashVisible) or a stashed window's activation | |
| 150 | + | // (Activate); 0 without. Bound to that window, | |
| 150 | 151 | // it ends with its cover. Focusing another window ends it too. | |
| 151 | 152 | stashOver WindowID | |
| 152 | 153 | // home is the monitor (key or connector) the workspace belongs to; "" | |
| @@ -392,7 +393,7 @@ func (w *Workspace) focusModal(from WindowID) { | |||
| 392 | 393 | if c := w.cover(); c != 0 && c != d { | |
| 393 | 394 | if i := w.floatIndex(d); i >= 0 && !w.overCover(w.Floats[i]) { | |
| 394 | 395 | // Hidden under another window's fullscreen: leave it, as | |
| 395 | - | // Activate does, so the dialog shows. | |
| 396 | + | // Activate does for a tile, so the dialog shows. | |
| 396 | 397 | w.leaveFullscreen() | |
| 397 | 398 | } | |
| 398 | 399 | } | |
| @@ -408,22 +409,29 @@ func (w *Workspace) overCover(f Float) bool { | |||
| 408 | 409 | if c == 0 || f.ID == c { | |
| 409 | 410 | return false | |
| 410 | 411 | } | |
| 411 | - | over := w.stashOverCover() | |
| 412 | + | owner := w.dialogOwner(f, c) | |
| 413 | + | return owner == c || owner != 0 && w.stashOverCover() && w.stashIndex(owner) >= 0 | |
| 414 | + | } | |
| 415 | + | ||
| 416 | + | // dialogOwner is the window float f is a dialog of, through dialogs of | |
| 417 | + | // dialogs: the first parent that is no float, or stop. 0 when f is no | |
| 418 | + | // dialog, its chain ends on a plain float, or it loops. | |
| 419 | + | func (w *Workspace) dialogOwner(f Float, stop WindowID) WindowID { | |
| 412 | 420 | // Each hop goes to another float: at most len(w.Floats) hops. | |
| 413 | 421 | for range len(w.Floats) { | |
| 414 | 422 | if f.over == 0 || f.over == f.ID { | |
| 415 | - | return false | |
| 423 | + | return 0 | |
| 416 | 424 | } | |
| 417 | - | if f.over == c || over && w.stashIndex(f.over) >= 0 { | |
| 418 | - | return true | |
| 425 | + | if f.over == stop { | |
| 426 | + | return stop | |
| 419 | 427 | } | |
| 420 | 428 | i := w.floatIndex(f.over) | |
| 421 | 429 | if i < 0 { | |
| 422 | - | return false | |
| 430 | + | return f.over | |
| 423 | 431 | } | |
| 424 | 432 | f = w.Floats[i] | |
| 425 | 433 | } | |
| 426 | - | return false | |
| 434 | + | return 0 | |
| 427 | 435 | } | |
| 428 | 436 | ||
| 429 | 437 | // coverDialog is the topmost dialog shown over the covering fullscreen | |
| @@ -1255,17 +1263,29 @@ func (w *Workspace) leaveFullscreen() { | |||
| 1255 | 1263 | } | |
| 1256 | 1264 | ||
| 1257 | 1265 | // Activate focuses a window and makes it visible: it leaves another | |
| 1258 | - | // window's fullscreen, which hides everything else. A stashed window | |
| 1259 | - | // shows over a covering window instead, as ToggleStashVisible does: the | |
| 1260 | - | // cover stays fullscreen behind, so a game never sees its fullscreen end. | |
| 1266 | + | // 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 | + | // shows over it instead, as ToggleStashVisible does: the cover stays | |
| 1269 | + | // fullscreen behind, so a game never sees its fullscreen end. | |
| 1261 | 1270 | func (w *Workspace) Activate(id WindowID) { | |
| 1262 | - | if i := w.stashIndex(id); i >= 0 && w.coverStays(id) { | |
| 1263 | - | if !w.stashOverCover() { | |
| 1264 | - | w.stashOver, w.stashHidden = w.cover(), false | |
| 1271 | + | if c := w.cover(); c != 0 && id != c { | |
| 1272 | + | owner := id | |
| 1273 | + | if f := w.floatIndex(id); f >= 0 { | |
| 1274 | + | owner = w.dialogOwner(w.Floats[f], c) | |
| 1275 | + | } | |
| 1276 | + | if i := w.stashIndex(owner); i >= 0 && w.coverStays(owner) { | |
| 1277 | + | if !w.stashOverCover() { | |
| 1278 | + | w.showStashOver() | |
| 1279 | + | } | |
| 1280 | + | w.stashAt, w.stashFocus, w.floatFocus = i, true, false | |
| 1281 | + | } | |
| 1282 | + | if owner == c || w.stashOverCover() && w.stashIndex(owner) >= 0 { | |
| 1283 | + | if owner != id { | |
| 1284 | + | w.FocusID(id) | |
| 1285 | + | } | |
| 1286 | + | w.focusModal(0) | |
| 1287 | + | return | |
| 1265 | 1288 | } | |
| 1266 | - | w.stashAt, w.stashFocus, w.floatFocus = i, true, false | |
| 1267 | - | w.focusModal(0) | |
| 1268 | - | return | |
| 1269 | 1289 | } | |
| 1270 | 1290 | if i := w.stashIndex(id); i >= 0 && w.stashHidden { | |
| 1271 | 1291 | // A hidden stashed window must be seen: show the stash with it, | |
| @@ -146,7 +146,8 @@ type Workspace struct { | |||||
| 146 | // is hidden, restored when shown. | 146 | // is hidden, restored when shown. | ||
| 147 | hiddenFullscreen WindowID | 147 | hiddenFullscreen WindowID | ||
| 148 | // stashOver is the covering fullscreen window the stash shows over, a | 148 | // stashOver is the covering fullscreen window the stash shows over, a | ||
| 149 | - | // user override (ToggleStashVisible); 0 without. Bound to that window, | 149 | + | // user override (ToggleStashVisible) or a stashed window's activation |
| 150 | + | // (Activate); 0 without. Bound to that window, | |||
| 150 | // it ends with its cover. Focusing another window ends it too. | 151 | // it ends with its cover. Focusing another window ends it too. | ||
| 151 | stashOver WindowID | 152 | stashOver WindowID | ||
| 152 | // home is the monitor (key or connector) the workspace belongs to; "" | 153 | // home is the monitor (key or connector) the workspace belongs to; "" | ||
| @@ -392,7 +393,7 @@ func (w *Workspace) focusModal(from WindowID) { | |||||
| 392 | if c := w.cover(); c != 0 && c != d { | 393 | if c := w.cover(); c != 0 && c != d { | ||
| 393 | 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]) { | ||
| 394 | // Hidden under another window's fullscreen: leave it, as | 395 | // Hidden under another window's fullscreen: leave it, as | ||
| 395 | - | // Activate does, so the dialog shows. | 396 | + | // Activate does for a tile, so the dialog shows. |
| 396 | w.leaveFullscreen() | 397 | w.leaveFullscreen() | ||
| 397 | } | 398 | } | ||
| 398 | } | 399 | } | ||
| @@ -408,22 +409,29 @@ func (w *Workspace) overCover(f Float) bool { | |||||
| 408 | if c == 0 || f.ID == c { | 409 | if c == 0 || f.ID == c { | ||
| 409 | return false | 410 | return false | ||
| 410 | } | 411 | } | ||
| 411 | - | over := w.stashOverCover() | 412 | + | owner := w.dialogOwner(f, c) |
| 413 | + | return owner == c || owner != 0 && w.stashOverCover() && w.stashIndex(owner) >= 0 | |||
| 414 | + | } | |||
| 415 | + | ||||
| 416 | + | // dialogOwner is the window float f is a dialog of, through dialogs of | |||
| 417 | + | // dialogs: the first parent that is no float, or stop. 0 when f is no | |||
| 418 | + | // dialog, its chain ends on a plain float, or it loops. | |||
| 419 | + | func (w *Workspace) dialogOwner(f Float, stop WindowID) WindowID { | |||
| 412 | // Each hop goes to another float: at most len(w.Floats) hops. | 420 | // Each hop goes to another float: at most len(w.Floats) hops. | ||
| 413 | for range len(w.Floats) { | 421 | for range len(w.Floats) { | ||
| 414 | if f.over == 0 || f.over == f.ID { | 422 | if f.over == 0 || f.over == f.ID { | ||
| 415 | - | return false | 423 | + | return 0 |
| 416 | } | 424 | } | ||
| 417 | - | if f.over == c || over && w.stashIndex(f.over) >= 0 { | 425 | + | if f.over == stop { |
| 418 | - | return true | 426 | + | return stop |
| 419 | } | 427 | } | ||
| 420 | i := w.floatIndex(f.over) | 428 | i := w.floatIndex(f.over) | ||
| 421 | if i < 0 { | 429 | if i < 0 { | ||
| 422 | - | return false | 430 | + | return f.over |
| 423 | } | 431 | } | ||
| 424 | f = w.Floats[i] | 432 | f = w.Floats[i] | ||
| 425 | } | 433 | } | ||
| 426 | - | return false | 434 | + | return 0 |
| 427 | } | 435 | } | ||
| 428 | 436 | ||||
| 429 | // coverDialog is the topmost dialog shown over the covering fullscreen | 437 | // coverDialog is the topmost dialog shown over the covering fullscreen | ||
| @@ -1255,17 +1263,29 @@ func (w *Workspace) leaveFullscreen() { | |||||
| 1255 | } | 1263 | } | ||
| 1256 | 1264 | ||||
| 1257 | // Activate focuses a window and makes it visible: it leaves another | 1265 | // Activate focuses a window and makes it visible: it leaves another | ||
| 1258 | - | // window's fullscreen, which hides everything else. A stashed window | 1266 | + | // window's fullscreen, which hides everything else. Over a covering |
| 1259 | - | // shows over a covering window instead, as ToggleStashVisible does: the | 1267 | + | // window, a dialog drawn over it, a stashed window or a dialog of one |
| 1260 | - | // cover stays fullscreen behind, so a game never sees its fullscreen end. | 1268 | + | // shows over it instead, as ToggleStashVisible does: the cover stays |
| 1269 | + | // fullscreen behind, so a game never sees its fullscreen end. | |||
| 1261 | func (w *Workspace) Activate(id WindowID) { | 1270 | func (w *Workspace) Activate(id WindowID) { | ||
| 1262 | - | if i := w.stashIndex(id); i >= 0 && w.coverStays(id) { | 1271 | + | if c := w.cover(); c != 0 && id != c { |
| 1263 | - | if !w.stashOverCover() { | 1272 | + | owner := id |
| 1264 | - | w.stashOver, w.stashHidden = w.cover(), false | 1273 | + | if f := w.floatIndex(id); f >= 0 { |
| 1274 | + | owner = w.dialogOwner(w.Floats[f], c) | |||
| 1275 | + | } | |||
| 1276 | + | if i := w.stashIndex(owner); i >= 0 && w.coverStays(owner) { | |||
| 1277 | + | if !w.stashOverCover() { | |||
| 1278 | + | w.showStashOver() | |||
| 1279 | + | } | |||
| 1280 | + | w.stashAt, w.stashFocus, w.floatFocus = i, true, false | |||
| 1281 | + | } | |||
| 1282 | + | if owner == c || w.stashOverCover() && w.stashIndex(owner) >= 0 { | |||
| 1283 | + | if owner != id { | |||
| 1284 | + | w.FocusID(id) | |||
| 1285 | + | } | |||
| 1286 | + | w.focusModal(0) | |||
| 1287 | + | return | |||
| 1265 | } | 1288 | } | ||
| 1266 | - | w.stashAt, w.stashFocus, w.floatFocus = i, true, false | |||
| 1267 | - | w.focusModal(0) | |||
| 1268 | - | return | |||
| 1269 | } | 1289 | } | ||
| 1270 | if i := w.stashIndex(id); i >= 0 && w.stashHidden { | 1290 | if i := w.stashIndex(id); i >= 0 && w.stashHidden { | ||
| 1271 | // A hidden stashed window must be seen: show the stash with it, | 1291 | // A hidden stashed window must be seen: show the stash with it, | ||
M internal/core/stash.go +9 -4
| @@ -174,9 +174,7 @@ func (w *Workspace) ToggleStashVisible() { | |||
| 174 | 174 | w.dropStashOver() | |
| 175 | 175 | return | |
| 176 | 176 | } | |
| 177 | - | // Not showStash: a pending stashed fullscreen must not replace | |
| 178 | - | // the covering window. | |
| 179 | - | w.stashOver, w.stashHidden, w.stashFocus, w.floatFocus = c, false, true, false | |
| 177 | + | w.showStashOver() | |
| 180 | 178 | return | |
| 181 | 179 | } | |
| 182 | 180 | if w.stashHidden { | |
| @@ -186,8 +184,15 @@ func (w *Workspace) ToggleStashVisible() { | |||
| 186 | 184 | w.stashHidden, w.stashFocus = true, false | |
| 187 | 185 | } | |
| 188 | 186 | ||
| 187 | + | // showStashOver shows the stash over the covering fullscreen window, | |
| 188 | + | // focused. Not showStash: a pending stashed fullscreen must not replace | |
| 189 | + | // the covering window. | |
| 190 | + | func (w *Workspace) showStashOver() { | |
| 191 | + | w.stashOver, w.stashHidden, w.stashFocus, w.floatFocus = w.cover(), false, true, false | |
| 192 | + | } | |
| 193 | + | ||
| 189 | 194 | // stashOverCover reports whether the stash shows over the covering | |
| 190 | - | // fullscreen window (ToggleStashVisible). | |
| 195 | + | // fullscreen window (ToggleStashVisible, Activate). | |
| 191 | 196 | func (w *Workspace) stashOverCover() bool { | |
| 192 | 197 | return w.stashOver != 0 && w.stashOver == w.cover() && !w.stashHidden && len(w.Stash) > 0 | |
| 193 | 198 | } | |
| @@ -174,9 +174,7 @@ func (w *Workspace) ToggleStashVisible() { | |||||
| 174 | w.dropStashOver() | 174 | w.dropStashOver() | ||
| 175 | return | 175 | return | ||
| 176 | } | 176 | } | ||
| 177 | - | // Not showStash: a pending stashed fullscreen must not replace | 177 | + | w.showStashOver() |
| 178 | - | // the covering window. | |||
| 179 | - | w.stashOver, w.stashHidden, w.stashFocus, w.floatFocus = c, false, true, false | |||
| 180 | return | 178 | return | ||
| 181 | } | 179 | } | ||
| 182 | if w.stashHidden { | 180 | if w.stashHidden { | ||
| @@ -186,8 +184,15 @@ func (w *Workspace) ToggleStashVisible() { | |||||
| 186 | w.stashHidden, w.stashFocus = true, false | 184 | w.stashHidden, w.stashFocus = true, false | ||
| 187 | } | 185 | } | ||
| 188 | 186 | ||||
| 187 | + | // showStashOver shows the stash over the covering fullscreen window, | |||
| 188 | + | // focused. Not showStash: a pending stashed fullscreen must not replace | |||
| 189 | + | // the covering window. | |||
| 190 | + | func (w *Workspace) showStashOver() { | |||
| 191 | + | w.stashOver, w.stashHidden, w.stashFocus, w.floatFocus = w.cover(), false, true, false | |||
| 192 | + | } | |||
| 193 | + | ||||
| 189 | // stashOverCover reports whether the stash shows over the covering | 194 | // stashOverCover reports whether the stash shows over the covering | ||
| 190 | - | // fullscreen window (ToggleStashVisible). | 195 | + | // fullscreen window (ToggleStashVisible, Activate). |
| 191 | func (w *Workspace) stashOverCover() bool { | 196 | func (w *Workspace) stashOverCover() bool { | ||
| 192 | return w.stashOver != 0 && w.stashOver == w.cover() && !w.stashHidden && len(w.Stash) > 0 | 197 | return w.stashOver != 0 && w.stashOver == w.cover() && !w.stashHidden && len(w.Stash) > 0 | ||
| 193 | } | 198 | } | ||
M internal/core/stash_cover_test.go +178 -0
| @@ -1,6 +1,8 @@ | |||
| 1 | 1 | package core | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 5 | + | "slices" | |
| 4 | 6 | "testing" | |
| 5 | 7 | ||
| 6 | 8 | "github.com/bnema/neferwl/internal/ports" | |
| @@ -158,3 +160,179 @@ func TestStashOverCoverLeaves(t *testing.T) { | |||
| 158 | 160 | t.Fatalf("fullscreen: cover %d left %d", w.cover(), w.left) | |
| 159 | 161 | } | |
| 160 | 162 | } | |
| 163 | + | ||
| 164 | + | // Activating a window over a covering fullscreen window (xdg-activation, | |
| 165 | + | // taskbar) never ends that fullscreen when the window can show over it: | |
| 166 | + | // a stashed window shows the stash over the cover, and dialogs of the | |
| 167 | + | // cover or of stashed windows, and their dialogs, show above. A game | |
| 168 | + | // told it left fullscreen (configure 0x0) quits. | |
| 169 | + | func TestActivateOverCover(t *testing.T) { | |
| 170 | + | covers := []struct { | |
| 171 | + | name string | |
| 172 | + | overflow Overflow | |
| 173 | + | float bool | |
| 174 | + | }{ | |
| 175 | + | {"fixed tile", OverflowFixed, false}, | |
| 176 | + | {"scroll tile", OverflowScroll, false}, | |
| 177 | + | {"fixed float", OverflowFixed, true}, | |
| 178 | + | {"scroll float", OverflowScroll, true}, | |
| 179 | + | } | |
| 180 | + | cases := []struct { | |
| 181 | + | name string | |
| 182 | + | // setup prepares windows and returns the window to activate. | |
| 183 | + | setup func(w *Workspace, cover WindowID) WindowID | |
| 184 | + | // focused is the window focused after the activation. | |
| 185 | + | focused WindowID | |
| 186 | + | // over reports whether the stash shows over the cover then. | |
| 187 | + | over bool | |
| 188 | + | }{ | |
| 189 | + | {"stashed", func(*Workspace, WindowID) WindowID { return 2 }, 2, true}, | |
| 190 | + | {"modal of stashed", func(w *Workspace, _ WindowID) WindowID { | |
| 191 | + | w.AddDialog(9, 2, 20, 10) | |
| 192 | + | w.SetModal(9, true) | |
| 193 | + | return 2 | |
| 194 | + | }, 9, true}, | |
| 195 | + | {"modal chain of stashed", func(w *Workspace, _ WindowID) WindowID { | |
| 196 | + | w.AddDialog(9, 2, 20, 10) | |
| 197 | + | w.SetModal(9, true) | |
| 198 | + | w.AddDialog(8, 9, 10, 5) | |
| 199 | + | w.SetModal(8, true) | |
| 200 | + | return 2 | |
| 201 | + | }, 8, true}, | |
| 202 | + | {"dialog of stashed", func(w *Workspace, _ WindowID) WindowID { | |
| 203 | + | w.AddDialog(9, 2, 20, 10) | |
| 204 | + | return 9 | |
| 205 | + | }, 9, true}, | |
| 206 | + | {"dialog of cover", func(w *Workspace, cover WindowID) WindowID { | |
| 207 | + | w.AddDialog(9, cover, 20, 10) | |
| 208 | + | w.FocusID(cover) | |
| 209 | + | return 9 | |
| 210 | + | }, 9, false}, | |
| 211 | + | {"modal chain of cover", func(w *Workspace, cover WindowID) WindowID { | |
| 212 | + | w.AddDialog(9, cover, 20, 10) | |
| 213 | + | w.SetModal(9, true) | |
| 214 | + | w.AddDialog(8, 9, 10, 5) | |
| 215 | + | w.SetModal(8, true) | |
| 216 | + | w.FocusID(cover) | |
| 217 | + | return cover | |
| 218 | + | }, 8, false}, | |
| 219 | + | {"stashed from a focused cover dialog", func(w *Workspace, cover WindowID) WindowID { | |
| 220 | + | w.ToggleStashVisible() | |
| 221 | + | w.AddDialog(9, cover, 20, 10) | |
| 222 | + | w.FocusID(9) | |
| 223 | + | return 2 | |
| 224 | + | }, 2, true}, | |
| 225 | + | } | |
| 226 | + | for _, cv := range covers { | |
| 227 | + | for _, tc := range cases { | |
| 228 | + | t.Run(cv.name+"/"+tc.name, func(t *testing.T) { | |
| 229 | + | _, w, cover := stashCoverCore(t, cv.overflow, cv.float) | |
| 230 | + | id := tc.setup(w, cover) | |
| 231 | + | view := w.View | |
| 232 | + | w.Activate(id) | |
| 233 | + | if got, _ := w.Focused(); got != tc.focused || w.cover() != cover || w.left != 0 || w.stashOverCover() != tc.over { | |
| 234 | + | t.Fatalf("focused %d (want %d) cover %d left %d over %v", got, tc.focused, w.cover(), w.left, w.stashOverCover()) | |
| 235 | + | } | |
| 236 | + | ps := w.Layout() | |
| 237 | + | if p := previewOf(t, ps, cover); p.Hidden || !p.Fullscreen { | |
| 238 | + | t.Fatalf("cover %+v", p) | |
| 239 | + | } | |
| 240 | + | if f, c := slices.IndexFunc(ps, isID(tc.focused)), slices.IndexFunc(ps, isID(cover)); f < c || ps[f].Hidden { | |
| 241 | + | t.Fatalf("focused window not over the cover: %+v", ps) | |
| 242 | + | } | |
| 243 | + | if !cv.float && w.View != view { | |
| 244 | + | t.Fatalf("view %d, was %d", w.View, view) | |
| 245 | + | } | |
| 246 | + | }) | |
| 247 | + | } | |
| 248 | + | } | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | // 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. | |
| 254 | + | 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()) | |
| 283 | + | } | |
| 284 | + | } | |
| 285 | + | ||
| 286 | + | // A dialog parent loop under a cover ends: neither dialog shows over it. | |
| 287 | + | func TestOverCoverDialogLoop(t *testing.T) { | |
| 288 | + | _, w, _ := stashCoverCore(t, OverflowFixed, true) | |
| 289 | + | w.AddFloating(8, 10, 5) | |
| 290 | + | w.AddFloating(9, 10, 5) | |
| 291 | + | w.SetDialogParent(8, 9) | |
| 292 | + | w.SetDialogParent(9, 8) | |
| 293 | + | ps := w.Layout() | |
| 294 | + | if !previewOf(t, ps, 8).Hidden || !previewOf(t, ps, 9).Hidden { | |
| 295 | + | t.Fatalf("looped dialogs shown: %+v", ps) | |
| 296 | + | } | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | // Through core: activating a stashed window keeps a fullscreen game's | |
| 300 | + | // configure fullscreen at the output size. | |
| 301 | + | func TestCoreActivateStashKeepsGameConfigure(t *testing.T) { | |
| 302 | + | cfg := ports.Config{} | |
| 303 | + | cfg.Keyboard.CmdKey = "super" | |
| 304 | + | cfg.Layout.MaxColumns = 2 | |
| 305 | + | commands := make(chan ports.ClientCommand, 256) | |
| 306 | + | c, err := New(cfg, Channels{Commands: commands, Scenes: make(chan []ports.Scene, 1)}, Options{}) | |
| 307 | + | if err != nil { | |
| 308 | + | t.Fatal(err) | |
| 309 | + | } | |
| 310 | + | c.addScreen(ports.OutputInfo{Name: "A", Width: 100, Height: 80}) | |
| 311 | + | m := c.screens[0].mon | |
| 312 | + | m.AddWindow(2) | |
| 313 | + | w := m.Current() | |
| 314 | + | w.ToggleWindowStash() | |
| 315 | + | w.ToggleStashVisible() | |
| 316 | + | m.AddFloating(4, 100, 80) | |
| 317 | + | m.SetFullscreen(4, true) | |
| 318 | + | ctx := context.Background() | |
| 319 | + | if err := c.activate(ctx, 2); err != nil { | |
| 320 | + | t.Fatal(err) | |
| 321 | + | } | |
| 322 | + | if err := c.publish(ctx); err != nil { | |
| 323 | + | t.Fatal(err) | |
| 324 | + | } | |
| 325 | + | var last *ports.ConfigureWindow | |
| 326 | + | for len(commands) > 0 { | |
| 327 | + | if v, ok := (<-commands).(ports.ConfigureWindow); ok && v.ID == 4 { | |
| 328 | + | last = &v | |
| 329 | + | } | |
| 330 | + | } | |
| 331 | + | if last == nil || !last.Fullscreen || last.Width != 100 || last.Height != 80 { | |
| 332 | + | t.Fatalf("game configure %+v", last) | |
| 333 | + | } | |
| 334 | + | } | |
| 335 | + | ||
| 336 | + | func isID(id WindowID) func(Placement) bool { | |
| 337 | + | return func(p Placement) bool { return p.ID == id } | |
| 338 | + | } | |
| @@ -1,6 +1,8 @@ | |||||
| 1 | package core | 1 | package core | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 5 | + | "slices" | |||
| 4 | "testing" | 6 | "testing" | ||
| 5 | 7 | ||||
| 6 | "github.com/bnema/neferwl/internal/ports" | 8 | "github.com/bnema/neferwl/internal/ports" | ||
| @@ -158,3 +160,179 @@ func TestStashOverCoverLeaves(t *testing.T) { | |||||
| 158 | t.Fatalf("fullscreen: cover %d left %d", w.cover(), w.left) | 160 | t.Fatalf("fullscreen: cover %d left %d", w.cover(), w.left) | ||
| 159 | } | 161 | } | ||
| 160 | } | 162 | } | ||
| 163 | + | ||||
| 164 | + | // Activating a window over a covering fullscreen window (xdg-activation, | |||
| 165 | + | // taskbar) never ends that fullscreen when the window can show over it: | |||
| 166 | + | // a stashed window shows the stash over the cover, and dialogs of the | |||
| 167 | + | // cover or of stashed windows, and their dialogs, show above. A game | |||
| 168 | + | // told it left fullscreen (configure 0x0) quits. | |||
| 169 | + | func TestActivateOverCover(t *testing.T) { | |||
| 170 | + | covers := []struct { | |||
| 171 | + | name string | |||
| 172 | + | overflow Overflow | |||
| 173 | + | float bool | |||
| 174 | + | }{ | |||
| 175 | + | {"fixed tile", OverflowFixed, false}, | |||
| 176 | + | {"scroll tile", OverflowScroll, false}, | |||
| 177 | + | {"fixed float", OverflowFixed, true}, | |||
| 178 | + | {"scroll float", OverflowScroll, true}, | |||
| 179 | + | } | |||
| 180 | + | cases := []struct { | |||
| 181 | + | name string | |||
| 182 | + | // setup prepares windows and returns the window to activate. | |||
| 183 | + | setup func(w *Workspace, cover WindowID) WindowID | |||
| 184 | + | // focused is the window focused after the activation. | |||
| 185 | + | focused WindowID | |||
| 186 | + | // over reports whether the stash shows over the cover then. | |||
| 187 | + | over bool | |||
| 188 | + | }{ | |||
| 189 | + | {"stashed", func(*Workspace, WindowID) WindowID { return 2 }, 2, true}, | |||
| 190 | + | {"modal of stashed", func(w *Workspace, _ WindowID) WindowID { | |||
| 191 | + | w.AddDialog(9, 2, 20, 10) | |||
| 192 | + | w.SetModal(9, true) | |||
| 193 | + | return 2 | |||
| 194 | + | }, 9, true}, | |||
| 195 | + | {"modal chain of stashed", func(w *Workspace, _ WindowID) WindowID { | |||
| 196 | + | w.AddDialog(9, 2, 20, 10) | |||
| 197 | + | w.SetModal(9, true) | |||
| 198 | + | w.AddDialog(8, 9, 10, 5) | |||
| 199 | + | w.SetModal(8, true) | |||
| 200 | + | return 2 | |||
| 201 | + | }, 8, true}, | |||
| 202 | + | {"dialog of stashed", func(w *Workspace, _ WindowID) WindowID { | |||
| 203 | + | w.AddDialog(9, 2, 20, 10) | |||
| 204 | + | return 9 | |||
| 205 | + | }, 9, true}, | |||
| 206 | + | {"dialog of cover", func(w *Workspace, cover WindowID) WindowID { | |||
| 207 | + | w.AddDialog(9, cover, 20, 10) | |||
| 208 | + | w.FocusID(cover) | |||
| 209 | + | return 9 | |||
| 210 | + | }, 9, false}, | |||
| 211 | + | {"modal chain of cover", func(w *Workspace, cover WindowID) WindowID { | |||
| 212 | + | w.AddDialog(9, cover, 20, 10) | |||
| 213 | + | w.SetModal(9, true) | |||
| 214 | + | w.AddDialog(8, 9, 10, 5) | |||
| 215 | + | w.SetModal(8, true) | |||
| 216 | + | w.FocusID(cover) | |||
| 217 | + | return cover | |||
| 218 | + | }, 8, false}, | |||
| 219 | + | {"stashed from a focused cover dialog", func(w *Workspace, cover WindowID) WindowID { | |||
| 220 | + | w.ToggleStashVisible() | |||
| 221 | + | w.AddDialog(9, cover, 20, 10) | |||
| 222 | + | w.FocusID(9) | |||
| 223 | + | return 2 | |||
| 224 | + | }, 2, true}, | |||
| 225 | + | } | |||
| 226 | + | for _, cv := range covers { | |||
| 227 | + | for _, tc := range cases { | |||
| 228 | + | t.Run(cv.name+"/"+tc.name, func(t *testing.T) { | |||
| 229 | + | _, w, cover := stashCoverCore(t, cv.overflow, cv.float) | |||
| 230 | + | id := tc.setup(w, cover) | |||
| 231 | + | view := w.View | |||
| 232 | + | w.Activate(id) | |||
| 233 | + | if got, _ := w.Focused(); got != tc.focused || w.cover() != cover || w.left != 0 || w.stashOverCover() != tc.over { | |||
| 234 | + | t.Fatalf("focused %d (want %d) cover %d left %d over %v", got, tc.focused, w.cover(), w.left, w.stashOverCover()) | |||
| 235 | + | } | |||
| 236 | + | ps := w.Layout() | |||
| 237 | + | if p := previewOf(t, ps, cover); p.Hidden || !p.Fullscreen { | |||
| 238 | + | t.Fatalf("cover %+v", p) | |||
| 239 | + | } | |||
| 240 | + | if f, c := slices.IndexFunc(ps, isID(tc.focused)), slices.IndexFunc(ps, isID(cover)); f < c || ps[f].Hidden { | |||
| 241 | + | t.Fatalf("focused window not over the cover: %+v", ps) | |||
| 242 | + | } | |||
| 243 | + | if !cv.float && w.View != view { | |||
| 244 | + | t.Fatalf("view %d, was %d", w.View, view) | |||
| 245 | + | } | |||
| 246 | + | }) | |||
| 247 | + | } | |||
| 248 | + | } | |||
| 249 | + | } | |||
| 250 | + | ||||
| 251 | + | // 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. | |||
| 254 | + | 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()) | |||
| 283 | + | } | |||
| 284 | + | } | |||
| 285 | + | ||||
| 286 | + | // A dialog parent loop under a cover ends: neither dialog shows over it. | |||
| 287 | + | func TestOverCoverDialogLoop(t *testing.T) { | |||
| 288 | + | _, w, _ := stashCoverCore(t, OverflowFixed, true) | |||
| 289 | + | w.AddFloating(8, 10, 5) | |||
| 290 | + | w.AddFloating(9, 10, 5) | |||
| 291 | + | w.SetDialogParent(8, 9) | |||
| 292 | + | w.SetDialogParent(9, 8) | |||
| 293 | + | ps := w.Layout() | |||
| 294 | + | if !previewOf(t, ps, 8).Hidden || !previewOf(t, ps, 9).Hidden { | |||
| 295 | + | t.Fatalf("looped dialogs shown: %+v", ps) | |||
| 296 | + | } | |||
| 297 | + | } | |||
| 298 | + | ||||
| 299 | + | // Through core: activating a stashed window keeps a fullscreen game's | |||
| 300 | + | // configure fullscreen at the output size. | |||
| 301 | + | func TestCoreActivateStashKeepsGameConfigure(t *testing.T) { | |||
| 302 | + | cfg := ports.Config{} | |||
| 303 | + | cfg.Keyboard.CmdKey = "super" | |||
| 304 | + | cfg.Layout.MaxColumns = 2 | |||
| 305 | + | commands := make(chan ports.ClientCommand, 256) | |||
| 306 | + | c, err := New(cfg, Channels{Commands: commands, Scenes: make(chan []ports.Scene, 1)}, Options{}) | |||
| 307 | + | if err != nil { | |||
| 308 | + | t.Fatal(err) | |||
| 309 | + | } | |||
| 310 | + | c.addScreen(ports.OutputInfo{Name: "A", Width: 100, Height: 80}) | |||
| 311 | + | m := c.screens[0].mon | |||
| 312 | + | m.AddWindow(2) | |||
| 313 | + | w := m.Current() | |||
| 314 | + | w.ToggleWindowStash() | |||
| 315 | + | w.ToggleStashVisible() | |||
| 316 | + | m.AddFloating(4, 100, 80) | |||
| 317 | + | m.SetFullscreen(4, true) | |||
| 318 | + | ctx := context.Background() | |||
| 319 | + | if err := c.activate(ctx, 2); err != nil { | |||
| 320 | + | t.Fatal(err) | |||
| 321 | + | } | |||
| 322 | + | if err := c.publish(ctx); err != nil { | |||
| 323 | + | t.Fatal(err) | |||
| 324 | + | } | |||
| 325 | + | var last *ports.ConfigureWindow | |||
| 326 | + | for len(commands) > 0 { | |||
| 327 | + | if v, ok := (<-commands).(ports.ConfigureWindow); ok && v.ID == 4 { | |||
| 328 | + | last = &v | |||
| 329 | + | } | |||
| 330 | + | } | |||
| 331 | + | if last == nil || !last.Fullscreen || last.Width != 100 || last.Height != 80 { | |||
| 332 | + | t.Fatalf("game configure %+v", last) | |||
| 333 | + | } | |||
| 334 | + | } | |||
| 335 | + | ||||
| 336 | + | func isID(id WindowID) func(Placement) bool { | |||
| 337 | + | return func(p Placement) bool { return p.ID == id } | |||
| 338 | + | } | |||