Merge pull request 'fix(core): keep a fullscreen cover when a stashed window or dialog is activated' (#42) from fix/fullscreen-stash into main
Reviewed-on: https://git.bnema.dev/bnema/neferwl/pulls/42
8 files · +356 -61
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, 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 | - | | `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, 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. 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/adapters/wayland/foreigntoplevel.go +1 -0
| @@ -267,6 +267,7 @@ func (*toplevelRequests) Destroy(*wlr.ZwlrForeignToplevelHandleV1) {} | |||
| 267 | 267 | ||
| 268 | 268 | func (t *toplevelRequests) Activate(*wlr.ZwlrForeignToplevelHandleV1, *wayland.Seat) { | |
| 269 | 269 | if w := t.window(); w != nil { | |
| 270 | + | t.s.log.Info().Uint64("id", uint64(w.id)).Msg("foreign toplevel activation") | |
| 270 | 271 | t.s.emit(ports.WindowActivate{ID: w.id}) | |
| 271 | 272 | } | |
| 272 | 273 | } | |
| @@ -267,6 +267,7 @@ func (*toplevelRequests) Destroy(*wlr.ZwlrForeignToplevelHandleV1) {} | |||||
| 267 | 267 | ||||
| 268 | func (t *toplevelRequests) Activate(*wlr.ZwlrForeignToplevelHandleV1, *wayland.Seat) { | 268 | func (t *toplevelRequests) Activate(*wlr.ZwlrForeignToplevelHandleV1, *wayland.Seat) { | ||
| 269 | if w := t.window(); w != nil { | 269 | if w := t.window(); w != nil { | ||
| 270 | + | t.s.log.Info().Uint64("id", uint64(w.id)).Msg("foreign toplevel activation") | |||
| 270 | t.s.emit(ports.WindowActivate{ID: w.id}) | 271 | t.s.emit(ports.WindowActivate{ID: w.id}) | ||
| 271 | } | 272 | } | ||
| 272 | } | 273 | } | ||
M internal/core/floating_test.go +2 -10
| @@ -490,8 +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 and focuses it; the | |
| 494 | - | // fullscreen window it hid behind leaves fullscreen. | |
| 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. | |
| 495 | 495 | func TestActivateHiddenFloat(t *testing.T) { | |
| 496 | 496 | w := workspace() | |
| 497 | 497 | w.AddWindow(1) | |
| @@ -499,14 +499,6 @@ func TestActivateHiddenFloat(t *testing.T) { | |||
| 499 | 499 | w.ToggleWindowStash() | |
| 500 | 500 | w.ToggleStashVisible() | |
| 501 | 501 | w.FocusID(1) | |
| 502 | - | w.SetFullscreen(1, true) | |
| 503 | - | w.Activate(2) | |
| 504 | - | if id, _ := w.Focused(); id != 2 || w.stashHidden || w.fullscreen != 0 { | |
| 505 | - | t.Fatalf("focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen) | |
| 506 | - | } | |
| 507 | - | // The float's own fullscreen, asked while hidden, comes back with it. | |
| 508 | - | w.FocusID(2) | |
| 509 | - | w.ToggleStashVisible() | |
| 510 | 502 | w.SetFullscreen(2, true) | |
| 511 | 503 | w.SetFullscreen(1, true) | |
| 512 | 504 | w.Activate(2) | |
| @@ -490,8 +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 and focuses it; the | 493 | + | // A stashed float's own fullscreen, asked while the stash was hidden, |
| 494 | - | // fullscreen window it hid behind leaves fullscreen. | 494 | + | // comes back with it when it is activated: it replaces the cover. |
| 495 | func TestActivateHiddenFloat(t *testing.T) { | 495 | func TestActivateHiddenFloat(t *testing.T) { | ||
| 496 | w := workspace() | 496 | w := workspace() | ||
| 497 | w.AddWindow(1) | 497 | w.AddWindow(1) | ||
| @@ -499,14 +499,6 @@ func TestActivateHiddenFloat(t *testing.T) { | |||||
| 499 | w.ToggleWindowStash() | 499 | w.ToggleWindowStash() | ||
| 500 | w.ToggleStashVisible() | 500 | w.ToggleStashVisible() | ||
| 501 | w.FocusID(1) | 501 | w.FocusID(1) | ||
| 502 | - | w.SetFullscreen(1, true) | |||
| 503 | - | w.Activate(2) | |||
| 504 | - | if id, _ := w.Focused(); id != 2 || w.stashHidden || w.fullscreen != 0 { | |||
| 505 | - | t.Fatalf("focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen) | |||
| 506 | - | } | |||
| 507 | - | // The float's own fullscreen, asked while hidden, comes back with it. | |||
| 508 | - | w.FocusID(2) | |||
| 509 | - | w.ToggleStashVisible() | |||
| 510 | w.SetFullscreen(2, true) | 502 | w.SetFullscreen(2, true) | ||
| 511 | w.SetFullscreen(1, true) | 503 | w.SetFullscreen(1, true) | ||
| 512 | w.Activate(2) | 504 | w.Activate(2) | ||
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 leaves another window's fullscreen | |
| 116 | - | // with the latch too. | |
| 117 | - | func TestActivateHiddenStashLatches(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 != 0 || w.left != 1 { | |
| 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 leaves another window's fullscreen | |||
| 116 | - | // with the latch too. | |||
| 117 | - | func TestActivateHiddenStashLatches(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 != 0 || w.left != 1 { | |||
| 131 | - | t.Fatalf("focused %d, fullscreen %d, left %d", f, w.fullscreen, w.left) | |||
| 132 | - | } | |||
| 133 | - | } | |||
M internal/core/layout.go +89 -19
| @@ -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; "" | |
| @@ -390,31 +391,59 @@ func (w *Workspace) focusModal(from WindowID) { | |||
| 390 | 391 | return | |
| 391 | 392 | } | |
| 392 | 393 | if c := w.cover(); c != 0 && c != d { | |
| 393 | - | if i := w.floatIndex(d); i >= 0 && !w.Floats[i].coverDialog(c) { | |
| 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 window not shown over the cover, so | |
| 397 | + | // the dialog shows. | |
| 396 | 398 | w.leaveFullscreen() | |
| 397 | 399 | } | |
| 398 | 400 | } | |
| 399 | 401 | w.FocusID(d) | |
| 400 | 402 | } | |
| 401 | 403 | ||
| 402 | - | // coverDialog reports whether f is a dialog of the covering fullscreen | |
| 403 | - | // window cover: the one float drawn over it. | |
| 404 | - | func (f Float) coverDialog(cover WindowID) bool { | |
| 405 | - | return cover != 0 && f.over == cover && f.ID != cover | |
| 404 | + | // overCover reports whether float f is drawn over the covering fullscreen | |
| 405 | + | // window: a dialog of the cover, or of a stashed window while the stash | |
| 406 | + | // shows over the cover (a "Save as…" of the app the user picked there), | |
| 407 | + | // or a dialog of such a dialog (an "Overwrite?" over the "Save as…"). | |
| 408 | + | func (w *Workspace) overCover(f Float) bool { | |
| 409 | + | c := w.cover() | |
| 410 | + | if c == 0 || f.ID == c { | |
| 411 | + | return false | |
| 412 | + | } | |
| 413 | + | owner := w.dialogOwner(f, c) | |
| 414 | + | return owner == c || owner != 0 && w.stashOverCover() && w.stashIndex(owner) >= 0 | |
| 415 | + | } | |
| 416 | + | ||
| 417 | + | // dialogOwner is the window float f is a dialog of, through dialogs of | |
| 418 | + | // dialogs: the first parent that is no float, or stop. 0 when f is no | |
| 419 | + | // dialog, its chain ends on a plain float, or it loops. | |
| 420 | + | func (w *Workspace) dialogOwner(f Float, stop WindowID) WindowID { | |
| 421 | + | // Each hop goes to another float: at most len(w.Floats) hops. | |
| 422 | + | for range len(w.Floats) { | |
| 423 | + | if f.over == 0 || f.over == f.ID { | |
| 424 | + | return 0 | |
| 425 | + | } | |
| 426 | + | if f.over == stop { | |
| 427 | + | return stop | |
| 428 | + | } | |
| 429 | + | i := w.floatIndex(f.over) | |
| 430 | + | if i < 0 { | |
| 431 | + | return f.over | |
| 432 | + | } | |
| 433 | + | f = w.Floats[i] | |
| 434 | + | } | |
| 435 | + | return 0 | |
| 406 | 436 | } | |
| 407 | 437 | ||
| 408 | 438 | // coverDialog is the topmost dialog shown over the covering fullscreen | |
| 409 | - | // window when the floats have the focus, else 0. The floats it hides cannot | |
| 410 | - | // take the focus. | |
| 439 | + | // window (overCover) when the floats have the focus, else 0. The floats it | |
| 440 | + | // hides cannot take the focus. | |
| 411 | 441 | func (w *Workspace) coverDialog() WindowID { | |
| 412 | 442 | if !w.floatFocus { | |
| 413 | 443 | return 0 | |
| 414 | 444 | } | |
| 415 | - | cover := w.cover() | |
| 416 | 445 | for i := len(w.Floats) - 1; i >= 0; i-- { | |
| 417 | - | if f := w.Floats[i]; !f.below && f.coverDialog(cover) { | |
| 446 | + | if f := w.Floats[i]; !f.below && w.overCover(f) { | |
| 418 | 447 | return f.ID | |
| 419 | 448 | } | |
| 420 | 449 | } | |
| @@ -638,9 +667,9 @@ func (w *Workspace) FocusID(id WindowID) bool { | |||
| 638 | 667 | f := w.Floats[i] | |
| 639 | 668 | f.below = false | |
| 640 | 669 | w.Floats = append(slices.Delete(w.Floats, i, i+1), f) | |
| 641 | - | if !f.coverDialog(w.cover()) { | |
| 642 | - | // A dialog of the cover shows over the stash; leaving it | |
| 643 | - | // returns there. | |
| 670 | + | if !w.overCover(f) { | |
| 671 | + | // A dialog shown over the cover keeps the stash over it; | |
| 672 | + | // leaving it returns there. | |
| 644 | 673 | w.dropStashOver() | |
| 645 | 674 | } | |
| 646 | 675 | w.floatFocus = true | |
| @@ -679,7 +708,8 @@ func (w *Workspace) FocusColumn(dir int) bool { | |||
| 679 | 708 | from, _ := w.Focused() | |
| 680 | 709 | defer w.focusModal(from) | |
| 681 | 710 | if w.coverDialogFocused() { | |
| 682 | - | // The first move goes back to the fullscreen window under it. | |
| 711 | + | // The first move goes back to the window under it: the stash | |
| 712 | + | // over the cover, or the cover. | |
| 683 | 713 | w.floatFocus = false | |
| 684 | 714 | return true | |
| 685 | 715 | } | |
| @@ -1234,8 +1264,39 @@ func (w *Workspace) leaveFullscreen() { | |||
| 1234 | 1264 | } | |
| 1235 | 1265 | ||
| 1236 | 1266 | // Activate focuses a window and makes it visible: it leaves another | |
| 1237 | - | // window's fullscreen, which hides everything else. | |
| 1267 | + | // window's fullscreen, which hides everything else. Over a covering | |
| 1268 | + | // window, a dialog drawn over it, a stashed window or a dialog of one | |
| 1269 | + | // shows over it instead, as ToggleStashVisible does: the cover stays | |
| 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. | |
| 1238 | 1274 | func (w *Workspace) Activate(id WindowID) { | |
| 1275 | + | if c := w.cover(); c != 0 && id != c { | |
| 1276 | + | owner := id | |
| 1277 | + | if f := w.floatIndex(id); f >= 0 { | |
| 1278 | + | owner = w.dialogOwner(w.Floats[f], c) | |
| 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 | + | } | |
| 1286 | + | if i := w.stashIndex(owner); i >= 0 && w.coverStays(owner) { | |
| 1287 | + | if !w.stashOverCover() { | |
| 1288 | + | w.showStashOver() | |
| 1289 | + | } | |
| 1290 | + | w.stashAt, w.stashFocus, w.floatFocus = i, true, false | |
| 1291 | + | } | |
| 1292 | + | if owner == c || w.stashOverCover() && w.stashIndex(owner) >= 0 { | |
| 1293 | + | if owner != id { | |
| 1294 | + | w.FocusID(id) | |
| 1295 | + | } | |
| 1296 | + | w.focusModal(0) | |
| 1297 | + | return | |
| 1298 | + | } | |
| 1299 | + | } | |
| 1239 | 1300 | if i := w.stashIndex(id); i >= 0 && w.stashHidden { | |
| 1240 | 1301 | // A hidden stashed window must be seen: show the stash with it, | |
| 1241 | 1302 | // selected. Another window's fullscreen leaves first, so its own | |
| @@ -1251,6 +1312,14 @@ func (w *Workspace) Activate(id WindowID) { | |||
| 1251 | 1312 | w.focusModal(0) | |
| 1252 | 1313 | } | |
| 1253 | 1314 | ||
| 1315 | + | // coverStays reports whether activating stashed window id keeps the | |
| 1316 | + | // covering window fullscreen under the stash: a cover outside the stash, | |
| 1317 | + | // and no pending fullscreen of id's own that would replace it. | |
| 1318 | + | func (w *Workspace) coverStays(id WindowID) bool { | |
| 1319 | + | c := w.cover() | |
| 1320 | + | return c != 0 && w.stashIndex(c) < 0 && !(w.stashHidden && w.hiddenFullscreen == id) | |
| 1321 | + | } | |
| 1322 | + | ||
| 1254 | 1323 | // SetFullscreen applies a client request. It never moves focus: client | |
| 1255 | 1324 | // requests are automatic events (ADR 011 golden rule). | |
| 1256 | 1325 | func (w *Workspace) SetFullscreen(id WindowID, on bool) { | |
| @@ -1770,9 +1839,10 @@ func (w *Workspace) appendLayout(dst []Placement) []Placement { | |||
| 1770 | 1839 | if order != group { | |
| 1771 | 1840 | continue | |
| 1772 | 1841 | } | |
| 1773 | - | // A covering fullscreen window hides every float but its own | |
| 1774 | - | // dialogs: the others show again when it leaves fullscreen. | |
| 1775 | - | p := Placement{ID: f.ID, Rect: w.floatRect(f), Floating: true, Below: below, Focused: f.ID == focusedID, Inset: ports.SideAll, Hidden: cover != 0 && f.ID != cover && !f.coverDialog(cover)} | |
| 1842 | + | // A covering fullscreen window hides every float but the | |
| 1843 | + | // dialogs over it (overCover): the others show again when it | |
| 1844 | + | // leaves fullscreen. | |
| 1845 | + | p := Placement{ID: f.ID, Rect: w.floatRect(f), Floating: true, Below: below, Focused: f.ID == focusedID, Inset: ports.SideAll, Hidden: cover != 0 && f.ID != cover && !w.overCover(f)} | |
| 1776 | 1846 | if p.Hidden { | |
| 1777 | 1847 | p.Rect = Rect{} | |
| 1778 | 1848 | result = append(result, p) | |
| @@ -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; "" | ||
| @@ -390,31 +391,59 @@ func (w *Workspace) focusModal(from WindowID) { | |||||
| 390 | return | 391 | return | ||
| 391 | } | 392 | } | ||
| 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.Floats[i].coverDialog(c) { | 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 window not shown over the cover, so |
| 397 | + | // the dialog shows. | |||
| 396 | w.leaveFullscreen() | 398 | w.leaveFullscreen() | ||
| 397 | } | 399 | } | ||
| 398 | } | 400 | } | ||
| 399 | w.FocusID(d) | 401 | w.FocusID(d) | ||
| 400 | } | 402 | } | ||
| 401 | 403 | ||||
| 402 | - | // coverDialog reports whether f is a dialog of the covering fullscreen | 404 | + | // overCover reports whether float f is drawn over the covering fullscreen |
| 403 | - | // window cover: the one float drawn over it. | 405 | + | // window: a dialog of the cover, or of a stashed window while the stash |
| 404 | - | func (f Float) coverDialog(cover WindowID) bool { | 406 | + | // shows over the cover (a "Save as…" of the app the user picked there), |
| 405 | - | return cover != 0 && f.over == cover && f.ID != cover | 407 | + | // or a dialog of such a dialog (an "Overwrite?" over the "Save as…"). |
| 408 | + | func (w *Workspace) overCover(f Float) bool { | |||
| 409 | + | c := w.cover() | |||
| 410 | + | if c == 0 || f.ID == c { | |||
| 411 | + | return false | |||
| 412 | + | } | |||
| 413 | + | owner := w.dialogOwner(f, c) | |||
| 414 | + | return owner == c || owner != 0 && w.stashOverCover() && w.stashIndex(owner) >= 0 | |||
| 415 | + | } | |||
| 416 | + | ||||
| 417 | + | // dialogOwner is the window float f is a dialog of, through dialogs of | |||
| 418 | + | // dialogs: the first parent that is no float, or stop. 0 when f is no | |||
| 419 | + | // dialog, its chain ends on a plain float, or it loops. | |||
| 420 | + | func (w *Workspace) dialogOwner(f Float, stop WindowID) WindowID { | |||
| 421 | + | // Each hop goes to another float: at most len(w.Floats) hops. | |||
| 422 | + | for range len(w.Floats) { | |||
| 423 | + | if f.over == 0 || f.over == f.ID { | |||
| 424 | + | return 0 | |||
| 425 | + | } | |||
| 426 | + | if f.over == stop { | |||
| 427 | + | return stop | |||
| 428 | + | } | |||
| 429 | + | i := w.floatIndex(f.over) | |||
| 430 | + | if i < 0 { | |||
| 431 | + | return f.over | |||
| 432 | + | } | |||
| 433 | + | f = w.Floats[i] | |||
| 434 | + | } | |||
| 435 | + | return 0 | |||
| 406 | } | 436 | } | ||
| 407 | 437 | ||||
| 408 | // coverDialog is the topmost dialog shown over the covering fullscreen | 438 | // coverDialog is the topmost dialog shown over the covering fullscreen | ||
| 409 | - | // window when the floats have the focus, else 0. The floats it hides cannot | 439 | + | // window (overCover) when the floats have the focus, else 0. The floats it |
| 410 | - | // take the focus. | 440 | + | // hides cannot take the focus. |
| 411 | func (w *Workspace) coverDialog() WindowID { | 441 | func (w *Workspace) coverDialog() WindowID { | ||
| 412 | if !w.floatFocus { | 442 | if !w.floatFocus { | ||
| 413 | return 0 | 443 | return 0 | ||
| 414 | } | 444 | } | ||
| 415 | - | cover := w.cover() | |||
| 416 | for i := len(w.Floats) - 1; i >= 0; i-- { | 445 | for i := len(w.Floats) - 1; i >= 0; i-- { | ||
| 417 | - | if f := w.Floats[i]; !f.below && f.coverDialog(cover) { | 446 | + | if f := w.Floats[i]; !f.below && w.overCover(f) { |
| 418 | return f.ID | 447 | return f.ID | ||
| 419 | } | 448 | } | ||
| 420 | } | 449 | } | ||
| @@ -638,9 +667,9 @@ func (w *Workspace) FocusID(id WindowID) bool { | |||||
| 638 | f := w.Floats[i] | 667 | f := w.Floats[i] | ||
| 639 | f.below = false | 668 | f.below = false | ||
| 640 | w.Floats = append(slices.Delete(w.Floats, i, i+1), f) | 669 | w.Floats = append(slices.Delete(w.Floats, i, i+1), f) | ||
| 641 | - | if !f.coverDialog(w.cover()) { | 670 | + | if !w.overCover(f) { |
| 642 | - | // A dialog of the cover shows over the stash; leaving it | 671 | + | // A dialog shown over the cover keeps the stash over it; |
| 643 | - | // returns there. | 672 | + | // leaving it returns there. |
| 644 | w.dropStashOver() | 673 | w.dropStashOver() | ||
| 645 | } | 674 | } | ||
| 646 | w.floatFocus = true | 675 | w.floatFocus = true | ||
| @@ -679,7 +708,8 @@ func (w *Workspace) FocusColumn(dir int) bool { | |||||
| 679 | from, _ := w.Focused() | 708 | from, _ := w.Focused() | ||
| 680 | defer w.focusModal(from) | 709 | defer w.focusModal(from) | ||
| 681 | if w.coverDialogFocused() { | 710 | if w.coverDialogFocused() { | ||
| 682 | - | // The first move goes back to the fullscreen window under it. | 711 | + | // The first move goes back to the window under it: the stash |
| 712 | + | // over the cover, or the cover. | |||
| 683 | w.floatFocus = false | 713 | w.floatFocus = false | ||
| 684 | return true | 714 | return true | ||
| 685 | } | 715 | } | ||
| @@ -1234,8 +1264,39 @@ func (w *Workspace) leaveFullscreen() { | |||||
| 1234 | } | 1264 | } | ||
| 1235 | 1265 | ||||
| 1236 | // Activate focuses a window and makes it visible: it leaves another | 1266 | // Activate focuses a window and makes it visible: it leaves another | ||
| 1237 | - | // window's fullscreen, which hides everything else. | 1267 | + | // window's fullscreen, which hides everything else. Over a covering |
| 1268 | + | // window, a dialog drawn over it, a stashed window or a dialog of one | |||
| 1269 | + | // shows over it instead, as ToggleStashVisible does: the cover stays | |||
| 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. | |||
| 1238 | func (w *Workspace) Activate(id WindowID) { | 1274 | func (w *Workspace) Activate(id WindowID) { | ||
| 1275 | + | if c := w.cover(); c != 0 && id != c { | |||
| 1276 | + | owner := id | |||
| 1277 | + | if f := w.floatIndex(id); f >= 0 { | |||
| 1278 | + | owner = w.dialogOwner(w.Floats[f], c) | |||
| 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 | + | } | |||
| 1286 | + | if i := w.stashIndex(owner); i >= 0 && w.coverStays(owner) { | |||
| 1287 | + | if !w.stashOverCover() { | |||
| 1288 | + | w.showStashOver() | |||
| 1289 | + | } | |||
| 1290 | + | w.stashAt, w.stashFocus, w.floatFocus = i, true, false | |||
| 1291 | + | } | |||
| 1292 | + | if owner == c || w.stashOverCover() && w.stashIndex(owner) >= 0 { | |||
| 1293 | + | if owner != id { | |||
| 1294 | + | w.FocusID(id) | |||
| 1295 | + | } | |||
| 1296 | + | w.focusModal(0) | |||
| 1297 | + | return | |||
| 1298 | + | } | |||
| 1299 | + | } | |||
| 1239 | if i := w.stashIndex(id); i >= 0 && w.stashHidden { | 1300 | if i := w.stashIndex(id); i >= 0 && w.stashHidden { | ||
| 1240 | // A hidden stashed window must be seen: show the stash with it, | 1301 | // A hidden stashed window must be seen: show the stash with it, | ||
| 1241 | // selected. Another window's fullscreen leaves first, so its own | 1302 | // selected. Another window's fullscreen leaves first, so its own | ||
| @@ -1251,6 +1312,14 @@ func (w *Workspace) Activate(id WindowID) { | |||||
| 1251 | w.focusModal(0) | 1312 | w.focusModal(0) | ||
| 1252 | } | 1313 | } | ||
| 1253 | 1314 | ||||
| 1315 | + | // coverStays reports whether activating stashed window id keeps the | |||
| 1316 | + | // covering window fullscreen under the stash: a cover outside the stash, | |||
| 1317 | + | // and no pending fullscreen of id's own that would replace it. | |||
| 1318 | + | func (w *Workspace) coverStays(id WindowID) bool { | |||
| 1319 | + | c := w.cover() | |||
| 1320 | + | return c != 0 && w.stashIndex(c) < 0 && !(w.stashHidden && w.hiddenFullscreen == id) | |||
| 1321 | + | } | |||
| 1322 | + | ||||
| 1254 | // SetFullscreen applies a client request. It never moves focus: client | 1323 | // SetFullscreen applies a client request. It never moves focus: client | ||
| 1255 | // requests are automatic events (ADR 011 golden rule). | 1324 | // requests are automatic events (ADR 011 golden rule). | ||
| 1256 | func (w *Workspace) SetFullscreen(id WindowID, on bool) { | 1325 | func (w *Workspace) SetFullscreen(id WindowID, on bool) { | ||
| @@ -1770,9 +1839,10 @@ func (w *Workspace) appendLayout(dst []Placement) []Placement { | |||||
| 1770 | if order != group { | 1839 | if order != group { | ||
| 1771 | continue | 1840 | continue | ||
| 1772 | } | 1841 | } | ||
| 1773 | - | // A covering fullscreen window hides every float but its own | 1842 | + | // A covering fullscreen window hides every float but the |
| 1774 | - | // dialogs: the others show again when it leaves fullscreen. | 1843 | + | // dialogs over it (overCover): the others show again when it |
| 1775 | - | p := Placement{ID: f.ID, Rect: w.floatRect(f), Floating: true, Below: below, Focused: f.ID == focusedID, Inset: ports.SideAll, Hidden: cover != 0 && f.ID != cover && !f.coverDialog(cover)} | 1844 | + | // leaves fullscreen. |
| 1845 | + | p := Placement{ID: f.ID, Rect: w.floatRect(f), Floating: true, Below: below, Focused: f.ID == focusedID, Inset: ports.SideAll, Hidden: cover != 0 && f.ID != cover && !w.overCover(f)} | |||
| 1776 | if p.Hidden { | 1846 | if p.Hidden { | ||
| 1777 | p.Rect = Rect{} | 1847 | p.Rect = Rect{} | ||
| 1778 | result = append(result, p) | 1848 | result = append(result, p) | ||
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 +247 -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,248 @@ 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 | + | // 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 | + | } | |
| 237 | + | view := w.View | |
| 238 | + | w.Activate(id) | |
| 239 | + | if got, _ := w.Focused(); got != tc.focused || w.cover() != cover || w.left != 0 || w.stashOverCover() != tc.over { | |
| 240 | + | t.Fatalf("focused %d (want %d) cover %d left %d over %v", got, tc.focused, w.cover(), w.left, w.stashOverCover()) | |
| 241 | + | } | |
| 242 | + | ps := w.Layout() | |
| 243 | + | if p := previewOf(t, ps, cover); p.Hidden || !p.Fullscreen { | |
| 244 | + | t.Fatalf("cover %+v", p) | |
| 245 | + | } | |
| 246 | + | f := slices.IndexFunc(ps, isID(tc.focused)) | |
| 247 | + | if f < slices.IndexFunc(ps, isID(cover)) || ps[f].Hidden { | |
| 248 | + | t.Fatalf("focused window not over the cover: %+v", ps) | |
| 249 | + | } | |
| 250 | + | if tc.over && f < slices.IndexFunc(ps, isID(2)) { | |
| 251 | + | t.Fatalf("focused window under the stash: %+v", ps) | |
| 252 | + | } | |
| 253 | + | if !cv.float && w.View != view { | |
| 254 | + | t.Fatalf("view %d, was %d", w.View, view) | |
| 255 | + | } | |
| 256 | + | }) | |
| 257 | + | } | |
| 258 | + | } | |
| 259 | + | } | |
| 260 | + | ||
| 261 | + | // After an activation shows the stash over the cover, hiding the stash | |
| 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. | |
| 265 | + | func TestActivateOverCoverReturns(t *testing.T) { | |
| 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 | + | }) | |
| 313 | + | } | |
| 314 | + | } | |
| 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 | + | ||
| 355 | + | // A dialog parent loop under a cover ends: neither dialog shows over it. | |
| 356 | + | func TestOverCoverDialogLoop(t *testing.T) { | |
| 357 | + | _, w, _ := stashCoverCore(t, OverflowFixed, true) | |
| 358 | + | w.AddFloating(8, 10, 5) | |
| 359 | + | w.AddFloating(9, 10, 5) | |
| 360 | + | w.SetDialogParent(8, 9) | |
| 361 | + | w.SetDialogParent(9, 8) | |
| 362 | + | ps := w.Layout() | |
| 363 | + | if !previewOf(t, ps, 8).Hidden || !previewOf(t, ps, 9).Hidden { | |
| 364 | + | t.Fatalf("looped dialogs shown: %+v", ps) | |
| 365 | + | } | |
| 366 | + | } | |
| 367 | + | ||
| 368 | + | // Through core: activating a stashed window keeps a fullscreen game's | |
| 369 | + | // configure fullscreen at the output size. | |
| 370 | + | func TestCoreActivateStashKeepsGameConfigure(t *testing.T) { | |
| 371 | + | cfg := ports.Config{} | |
| 372 | + | cfg.Keyboard.CmdKey = "super" | |
| 373 | + | cfg.Layout.MaxColumns = 2 | |
| 374 | + | commands := make(chan ports.ClientCommand, 256) | |
| 375 | + | c, err := New(cfg, Channels{Commands: commands, Scenes: make(chan []ports.Scene, 1)}, Options{}) | |
| 376 | + | if err != nil { | |
| 377 | + | t.Fatal(err) | |
| 378 | + | } | |
| 379 | + | c.addScreen(ports.OutputInfo{Name: "A", Width: 100, Height: 80}) | |
| 380 | + | m := c.screens[0].mon | |
| 381 | + | m.AddWindow(2) | |
| 382 | + | w := m.Current() | |
| 383 | + | w.ToggleWindowStash() | |
| 384 | + | w.ToggleStashVisible() | |
| 385 | + | m.AddFloating(4, 100, 80) | |
| 386 | + | m.SetFullscreen(4, true) | |
| 387 | + | ctx := context.Background() | |
| 388 | + | if err := c.activate(ctx, 2); err != nil { | |
| 389 | + | t.Fatal(err) | |
| 390 | + | } | |
| 391 | + | if err := c.publish(ctx); err != nil { | |
| 392 | + | t.Fatal(err) | |
| 393 | + | } | |
| 394 | + | var last *ports.ConfigureWindow | |
| 395 | + | for len(commands) > 0 { | |
| 396 | + | if v, ok := (<-commands).(ports.ConfigureWindow); ok && v.ID == 4 { | |
| 397 | + | last = &v | |
| 398 | + | } | |
| 399 | + | } | |
| 400 | + | if last == nil || !last.Fullscreen || last.Width != 100 || last.Height != 80 { | |
| 401 | + | t.Fatalf("game configure %+v", last) | |
| 402 | + | } | |
| 403 | + | } | |
| 404 | + | ||
| 405 | + | func isID(id WindowID) func(Placement) bool { | |
| 406 | + | return func(p Placement) bool { return p.ID == id } | |
| 407 | + | } | |
| @@ -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,248 @@ 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 | + | // 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 | + | } | |||
| 237 | + | view := w.View | |||
| 238 | + | w.Activate(id) | |||
| 239 | + | if got, _ := w.Focused(); got != tc.focused || w.cover() != cover || w.left != 0 || w.stashOverCover() != tc.over { | |||
| 240 | + | t.Fatalf("focused %d (want %d) cover %d left %d over %v", got, tc.focused, w.cover(), w.left, w.stashOverCover()) | |||
| 241 | + | } | |||
| 242 | + | ps := w.Layout() | |||
| 243 | + | if p := previewOf(t, ps, cover); p.Hidden || !p.Fullscreen { | |||
| 244 | + | t.Fatalf("cover %+v", p) | |||
| 245 | + | } | |||
| 246 | + | f := slices.IndexFunc(ps, isID(tc.focused)) | |||
| 247 | + | if f < slices.IndexFunc(ps, isID(cover)) || ps[f].Hidden { | |||
| 248 | + | t.Fatalf("focused window not over the cover: %+v", ps) | |||
| 249 | + | } | |||
| 250 | + | if tc.over && f < slices.IndexFunc(ps, isID(2)) { | |||
| 251 | + | t.Fatalf("focused window under the stash: %+v", ps) | |||
| 252 | + | } | |||
| 253 | + | if !cv.float && w.View != view { | |||
| 254 | + | t.Fatalf("view %d, was %d", w.View, view) | |||
| 255 | + | } | |||
| 256 | + | }) | |||
| 257 | + | } | |||
| 258 | + | } | |||
| 259 | + | } | |||
| 260 | + | ||||
| 261 | + | // After an activation shows the stash over the cover, hiding the stash | |||
| 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. | |||
| 265 | + | func TestActivateOverCoverReturns(t *testing.T) { | |||
| 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 | + | }) | |||
| 313 | + | } | |||
| 314 | + | } | |||
| 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 | + | ||||
| 355 | + | // A dialog parent loop under a cover ends: neither dialog shows over it. | |||
| 356 | + | func TestOverCoverDialogLoop(t *testing.T) { | |||
| 357 | + | _, w, _ := stashCoverCore(t, OverflowFixed, true) | |||
| 358 | + | w.AddFloating(8, 10, 5) | |||
| 359 | + | w.AddFloating(9, 10, 5) | |||
| 360 | + | w.SetDialogParent(8, 9) | |||
| 361 | + | w.SetDialogParent(9, 8) | |||
| 362 | + | ps := w.Layout() | |||
| 363 | + | if !previewOf(t, ps, 8).Hidden || !previewOf(t, ps, 9).Hidden { | |||
| 364 | + | t.Fatalf("looped dialogs shown: %+v", ps) | |||
| 365 | + | } | |||
| 366 | + | } | |||
| 367 | + | ||||
| 368 | + | // Through core: activating a stashed window keeps a fullscreen game's | |||
| 369 | + | // configure fullscreen at the output size. | |||
| 370 | + | func TestCoreActivateStashKeepsGameConfigure(t *testing.T) { | |||
| 371 | + | cfg := ports.Config{} | |||
| 372 | + | cfg.Keyboard.CmdKey = "super" | |||
| 373 | + | cfg.Layout.MaxColumns = 2 | |||
| 374 | + | commands := make(chan ports.ClientCommand, 256) | |||
| 375 | + | c, err := New(cfg, Channels{Commands: commands, Scenes: make(chan []ports.Scene, 1)}, Options{}) | |||
| 376 | + | if err != nil { | |||
| 377 | + | t.Fatal(err) | |||
| 378 | + | } | |||
| 379 | + | c.addScreen(ports.OutputInfo{Name: "A", Width: 100, Height: 80}) | |||
| 380 | + | m := c.screens[0].mon | |||
| 381 | + | m.AddWindow(2) | |||
| 382 | + | w := m.Current() | |||
| 383 | + | w.ToggleWindowStash() | |||
| 384 | + | w.ToggleStashVisible() | |||
| 385 | + | m.AddFloating(4, 100, 80) | |||
| 386 | + | m.SetFullscreen(4, true) | |||
| 387 | + | ctx := context.Background() | |||
| 388 | + | if err := c.activate(ctx, 2); err != nil { | |||
| 389 | + | t.Fatal(err) | |||
| 390 | + | } | |||
| 391 | + | if err := c.publish(ctx); err != nil { | |||
| 392 | + | t.Fatal(err) | |||
| 393 | + | } | |||
| 394 | + | var last *ports.ConfigureWindow | |||
| 395 | + | for len(commands) > 0 { | |||
| 396 | + | if v, ok := (<-commands).(ports.ConfigureWindow); ok && v.ID == 4 { | |||
| 397 | + | last = &v | |||
| 398 | + | } | |||
| 399 | + | } | |||
| 400 | + | if last == nil || !last.Fullscreen || last.Width != 100 || last.Height != 80 { | |||
| 401 | + | t.Fatalf("game configure %+v", last) | |||
| 402 | + | } | |||
| 403 | + | } | |||
| 404 | + | ||||
| 405 | + | func isID(id WindowID) func(Placement) bool { | |||
| 406 | + | return func(p Placement) bool { return p.ID == id } | |||
| 407 | + | } | |||
M internal/core/stash_test.go +5 -5
| @@ -319,9 +319,9 @@ func TestFullscreenStashEdgeGoesToMonitor(t *testing.T) { | |||
| 319 | 319 | } | |
| 320 | 320 | } | |
| 321 | 321 | ||
| 322 | - | // Activating a hidden stashed window over a scroll fullscreen tile ends | |
| 323 | - | // the fullscreen like any exit: the view leaves the fullscreen alignment. | |
| 324 | - | func TestActivateHiddenStashEndsScrollFullscreen(t *testing.T) { | |
| 322 | + | // Activating a hidden stashed window over a scroll fullscreen tile shows | |
| 323 | + | // the stash over it: the tile stays fullscreen and the view stays aligned. | |
| 324 | + | func TestActivateHiddenStashOverScrollFullscreen(t *testing.T) { | |
| 325 | 325 | w := workspace() | |
| 326 | 326 | w.AddWindow(1) | |
| 327 | 327 | w.AddWindow(2) | |
| @@ -331,8 +331,8 @@ func TestActivateHiddenStashEndsScrollFullscreen(t *testing.T) { | |||
| 331 | 331 | w.FocusID(2) | |
| 332 | 332 | w.SetFullscreen(2, true) | |
| 333 | 333 | w.Activate(3) | |
| 334 | - | if id, _ := w.Focused(); id != 3 || w.fullscreen != 0 { | |
| 335 | - | t.Fatalf("focused %d fullscreen %d", id, w.fullscreen) | |
| 334 | + | if id, _ := w.Focused(); id != 3 || w.fullscreen != 2 || !w.stashOverCover() { | |
| 335 | + | t.Fatalf("focused %d fullscreen %d over %v", id, w.fullscreen, w.stashOverCover()) | |
| 336 | 336 | } | |
| 337 | 337 | view := w.View | |
| 338 | 338 | w.scroll() | |
| @@ -319,9 +319,9 @@ func TestFullscreenStashEdgeGoesToMonitor(t *testing.T) { | |||||
| 319 | } | 319 | } | ||
| 320 | } | 320 | } | ||
| 321 | 321 | ||||
| 322 | - | // Activating a hidden stashed window over a scroll fullscreen tile ends | 322 | + | // Activating a hidden stashed window over a scroll fullscreen tile shows |
| 323 | - | // the fullscreen like any exit: the view leaves the fullscreen alignment. | 323 | + | // the stash over it: the tile stays fullscreen and the view stays aligned. |
| 324 | - | func TestActivateHiddenStashEndsScrollFullscreen(t *testing.T) { | 324 | + | func TestActivateHiddenStashOverScrollFullscreen(t *testing.T) { |
| 325 | w := workspace() | 325 | w := workspace() | ||
| 326 | w.AddWindow(1) | 326 | w.AddWindow(1) | ||
| 327 | w.AddWindow(2) | 327 | w.AddWindow(2) | ||
| @@ -331,8 +331,8 @@ func TestActivateHiddenStashEndsScrollFullscreen(t *testing.T) { | |||||
| 331 | w.FocusID(2) | 331 | w.FocusID(2) | ||
| 332 | w.SetFullscreen(2, true) | 332 | w.SetFullscreen(2, true) | ||
| 333 | w.Activate(3) | 333 | w.Activate(3) | ||
| 334 | - | if id, _ := w.Focused(); id != 3 || w.fullscreen != 0 { | 334 | + | if id, _ := w.Focused(); id != 3 || w.fullscreen != 2 || !w.stashOverCover() { |
| 335 | - | t.Fatalf("focused %d fullscreen %d", id, w.fullscreen) | 335 | + | t.Fatalf("focused %d fullscreen %d over %v", id, w.fullscreen, w.stashOverCover()) |
| 336 | } | 336 | } | ||
| 337 | view := w.View | 337 | view := w.View | ||
| 338 | w.scroll() | 338 | w.scroll() | ||