feat(core): give a maximized cascade column its own band
Cascade bands now pack columns by slots: a maximized column takes all MaxColumns slots, so it sits alone in its band and the following columns flow into the next bands. Maximization in cascade becomes a lasting column state, kept across focus and band moves, as in scroll overflow. The overview band cards for columns hidden behind a maximized cascade column are removed: nothing is hidden anymore.
- commit
- ba5aae9c4d6c932359ac5e241b9b5da7f999e3b0
- author
- brice · · 2026-10-13 13:20 UTC
- parent
- 5385c18
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
15 files · +177 -442
M docs/config.md +6 -6
| @@ -227,13 +227,13 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||
| 227 | 227 | ||
| 228 | 228 | Cascade is the default layout (`layout.overflow = cascade`); it can also be set per output or on a named workspace. Columns fill bands of up to `max-columns`, then continue in a fresh band below. The columns of a band share its row of cells (see [Column widths](#column-widths)); auto columns share it equally, so a lone column takes the full width until another joins its band. New columns append last; closing a column compacts the following bands. Each band fills the usable height after panel reservations. | |
| 229 | 229 | ||
| 230 | - | Left/right focus and moves stay in the band; at its edge, `focus-column-left/right` go first to the previous or next occupied workspace; past the first or last one, to the neighbor monitor on that side; with none there, to the trailing empty workspace; `move-column-left/right` go to the neighbor monitor, or, with none, follow column order into the adjacent band. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band; `move-window-up/down` takes the same path, moving the whole column to the adjacent band by swapping it with that column. In a partial last band that column is the closest one, so a column moved down and back up may land at another place in its band. Column widths are cells within the band (see [Column widths](#column-widths)); window height shares apply too. Maximizing a column hides its same-band siblings until focus moves or maximization ends. | |
| 230 | + | Left/right focus and moves stay in the band; at its edge, `focus-column-left/right` go first to the previous or next occupied workspace; past the first or last one, to the neighbor monitor on that side; with none there, to the trailing empty workspace; `move-column-left/right` go to the neighbor monitor, or, with none, follow column order into the adjacent band. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band; `move-window-up/down` takes the same path, moving the whole column to the adjacent band by swapping it with that column. In a partial last band that column is the closest one, so a column moved down and back up may land at another place in its band. Column widths are cells within the band (see [Column widths](#column-widths)); window height shares apply too. A maximized column takes a band of its own: the columns after it continue in the following bands, and unmaximizing packs them back. It stays maximized while focus moves. | |
| 231 | 231 | ||
| 232 | 232 | Three-finger vertical swipes scroll bands; horizontal swipes switch workspaces. Scroll and fixed workspaces retain their existing gesture axes. A switch keeps the source workspace's axis throughout its transition; the destination's axes apply to the next gesture. The `focus-workspace-prev/next` commands switch to the previous or next workspace, whether the transition is vertical or horizontal. Four-finger and stash gestures keep their existing behaviour. | |
| 233 | 233 | ||
| 234 | - | In the [overview](#overview), the selected band is centred at the usual overview zoom, whatever the band count. The other bands of the workspace sit above and below it, dimmed, and may run off screen. Neighbor workspaces sit left and right, fully dimmed, with their selected row level with the centred band. The stash pile stays left of the band, inside the workspace's slot. A maximized column shows the columns it hides as dimmed cards behind it, peeking out on their side of the band: the nearest one on each side, then the next ones stacked at the band's edge. Selecting a card (left/right, a three-finger swipe or a click on a visible card) moves the maximization to it; Return keeps it there and Escape gives it back. | |
| 234 | + | In the [overview](#overview), the selected band is centred at the usual overview zoom, whatever the band count. The other bands of the workspace sit above and below it, dimmed, and may run off screen. Neighbor workspaces sit left and right, fully dimmed, with their selected row level with the centred band. The stash pile stays left of the band, inside the workspace's slot. | |
| 235 | 235 | ||
| 236 | - | In the overview, up/down move to the previous or next band and stop at the first and last; with a stash card selected they browse the pile. Left/right move along the band; past its first column they enter the stash, or go to the previous workspace when there is none; past its last column they go to the next workspace (with the overview closed, focus goes to the previous or next occupied workspace at the band's edge, then to the neighbor monitor, then to the trailing empty workspace). Covering floats fan left and right of the band: left/right traverse them before entering the stash or changing workspace, and up/down do nothing on a float. `h`/`j`/`k`/`l` and the focus binds follow the arrows; `focus-workspace-prev/next` always change workspace, except over a selected stash card, where they browse the pile. Three-finger swipes follow the fingers: vertical moves one band (or one stash card) and horizontal one workspace per gesture, even over a stash card. Over a maximized column that hides cards, horizontal moves one step per gesture like left/right: the next card, then past the band's edge the stash or a workspace. A vertical wheel or continuous scroll changes workspace, except over a selected stash card, where it browses the pile; a horizontal one moves along the band. | |
| 236 | + | In the overview, up/down move to the previous or next band and stop at the first and last; with a stash card selected they browse the pile. Left/right move along the band; past its first column they enter the stash, or go to the previous workspace when there is none; past its last column they go to the next workspace (with the overview closed, focus goes to the previous or next occupied workspace at the band's edge, then to the neighbor monitor, then to the trailing empty workspace). Covering floats fan left and right of the band: left/right traverse them before entering the stash or changing workspace, and up/down do nothing on a float. `h`/`j`/`k`/`l` and the focus binds follow the arrows; `focus-workspace-prev/next` always change workspace, except over a selected stash card, where they browse the pile. Three-finger swipes follow the fingers: vertical moves one band (or one stash card) and horizontal one workspace per gesture, even over a stash card. A vertical wheel or continuous scroll changes workspace, except over a selected stash card, where it browses the pile; a horizontal one moves along the band. | |
| 237 | 237 | ||
| 238 | 238 | Directional focus past the edge of the content switches workspace along the layout's workspace axis: up/down in scroll and fixed overflow, left/right in cascade. | |
| 239 | 239 | ||
| @@ -263,9 +263,9 @@ In a row, the auto columns share the cells the set ones leave; when no cell is l | |||
| 263 | 263 | | `toggle-floating` | Make the focused tile a [free floating window](#free-floating-windows) at its current size, centred; on a floating window, send it back to its former place in the columns (a native dialog becomes a new column). Does nothing on a stashed or fullscreen window | | |
| 264 | 264 | | `toggle-stash-visible` | Hide the stash and give the focus back to the tiles, or show it again with the focus on its selected window. Native dialogs stay as they are. Over a fullscreen window, shows the stash on top with the focus; the window stays fullscreen behind (composed, so no direct scanout while the stash shows) and is back when the stash hides or the focus leaves it. `toggle-window-stash` or `toggle-fullscreen` on a stashed window there leaves that fullscreen for it. An activation of a stashed window (an app asking for attention, a taskbar click) shows the stash over the fullscreen window the same way. New windows never join or show over it; dialogs of the fullscreen window or of stashed windows, and their own dialogs, show above it | | |
| 265 | 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. Outside cascade, 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 | | |
| 268 | - | | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off. On a window that made itself fullscreen (e.g. a Wine app at monitor size), it first returns the window to its column | | |
| 268 | + | | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off; in cascade, the column takes a band of its own. On a window that made itself fullscreen (e.g. a Wine app at monitor size), it first returns the window to its column | | |
| 269 | 269 | | `cycle-column-width` | Grow the focused column by one cell, taking it from the widest other column of its row (see [Column widths](#column-widths)). When no column has a cell to give, every column of the row goes back to equal shares. In scroll overflow, the column grows to `max-columns` cells, then back to auto. A lone column or a full row cannot grow: it goes back to equal shares if its cells were set, and otherwise nothing changes. A spiral never changes | | |
| 270 | 270 | | `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor. In cascade overflow the edge is the band's, and past it comes the previous or next occupied workspace, then the neighbor monitor, then the trailing empty workspace. In fixed overflow, the neighbor on screen. From a fullscreen window, the neighbor column comes first and fullscreen ends; at the edge it stays | | |
| 271 | 271 | | `focus-window-up/down` | Focus in the column; past the edge, the previous or next workspace (in cascade overflow, the adjacent band, stopping at the first and last). In fixed overflow, the column on screen above or below comes first. Up at the top brings back a covering float put behind the columns. From a fullscreen window, a window above or below comes first and fullscreen ends | | |
| @@ -312,7 +312,7 @@ The drop can be on another monitor; focus follows the window. There, the middle | |||
| 312 | 312 | ||
| 313 | 313 | ### Overview | |
| 314 | 314 | ||
| 315 | - | `toggle-overview` shows scaled previews of the current workspace, including over fullscreen windows, over a background darkened by `floating.dim`. Windows keep their size and previews show their last frames. Covering floats and windows hidden behind a maximized column in `fixed` overflow appear as cards: the on-screen item is in front, hidden columns share one spiral-layout card, and in a vertical overview up to two cards behind peek above it and passed cards peek below (a Cascade overview fans them left and right). Neighbor workspaces show dimmed stacks. Each monitor has one overview, laid out along the navigation axis of the current workspace's layout: neighbors appear above and below a `scroll` or `fixed` workspace and left and right of a `cascade` one (see [Cascade](#cascade)); stepping to a workspace with another layout turns the overview. A named workspace appears directly below the numbered workspace from which its `workspace <name>` bind was invoked. Invoking it from another numbered workspace updates this placement; browsing the overview does not. Several named workspaces attached to the same numbered workspace follow configuration order, with rules across the axis (horizontal in a vertical overview, vertical in a side-by-side one) separating their group from numbered rows. Named-to-named binds reuse the source's numbered attachment. Unattached named workspaces follow the last occupied numbered row, or the first row if all numbered workspaces are empty. The selected workspace stays centred and its immediate neighbors appear beside it along that axis; in a vertical overview, rows and stash previews keep their full width. Empty workspaces are skipped unless currently selected or serving as the invocation anchor of a visible named row. Named workspaces remain outside the numbered list and normal previous/next workspace navigation. Smaller floats, such as dialogs, stay hidden. A fullscreen floating window, or any fullscreen window with `fixed` overflow, is its workspace's front card, with the windows it hides behind it; `scroll` overflow keeps its columns selectable in one card. Choosing the fullscreen window keeps it fullscreen; choosing another window leaves fullscreen. | |
| 315 | + | `toggle-overview` shows scaled previews of the current workspace, including over fullscreen windows, over a background darkened by `floating.dim`. Windows keep their size and previews show their last frames. Covering floats and windows hidden behind a maximized column in `fixed` overflow appear as cards: the on-screen item is in front, hidden columns share one spiral-layout card, and in a vertical overview up to two cards behind peek above it and passed cards peek below (a Cascade overview fans covering floats left and right). Neighbor workspaces show dimmed stacks. Each monitor has one overview, laid out along the navigation axis of the current workspace's layout: neighbors appear above and below a `scroll` or `fixed` workspace and left and right of a `cascade` one (see [Cascade](#cascade)); stepping to a workspace with another layout turns the overview. A named workspace appears directly below the numbered workspace from which its `workspace <name>` bind was invoked. Invoking it from another numbered workspace updates this placement; browsing the overview does not. Several named workspaces attached to the same numbered workspace follow configuration order, with rules across the axis (horizontal in a vertical overview, vertical in a side-by-side one) separating their group from numbered rows. Named-to-named binds reuse the source's numbered attachment. Unattached named workspaces follow the last occupied numbered row, or the first row if all numbered workspaces are empty. The selected workspace stays centred and its immediate neighbors appear beside it along that axis; in a vertical overview, rows and stash previews keep their full width. Empty workspaces are skipped unless currently selected or serving as the invocation anchor of a visible named row. Named workspaces remain outside the numbered list and normal previous/next workspace navigation. Smaller floats, such as dialogs, stay hidden. A fullscreen floating window, or any fullscreen window with `fixed` overflow, is its workspace's front card, with the windows it hides behind it; `scroll` overflow keeps its columns selectable in one card. Choosing the fullscreen window keeps it fullscreen; choosing another window leaves fullscreen. | |
| 316 | 316 | ||
| 317 | 317 | | Key | Action | | |
| 318 | 318 | | --- | --- | | |
| @@ -227,13 +227,13 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||||
| 227 | 227 | ||||
| 228 | Cascade is the default layout (`layout.overflow = cascade`); it can also be set per output or on a named workspace. Columns fill bands of up to `max-columns`, then continue in a fresh band below. The columns of a band share its row of cells (see [Column widths](#column-widths)); auto columns share it equally, so a lone column takes the full width until another joins its band. New columns append last; closing a column compacts the following bands. Each band fills the usable height after panel reservations. | 228 | Cascade is the default layout (`layout.overflow = cascade`); it can also be set per output or on a named workspace. Columns fill bands of up to `max-columns`, then continue in a fresh band below. The columns of a band share its row of cells (see [Column widths](#column-widths)); auto columns share it equally, so a lone column takes the full width until another joins its band. New columns append last; closing a column compacts the following bands. Each band fills the usable height after panel reservations. | ||
| 229 | 229 | ||||
| 230 | - | Left/right focus and moves stay in the band; at its edge, `focus-column-left/right` go first to the previous or next occupied workspace; past the first or last one, to the neighbor monitor on that side; with none there, to the trailing empty workspace; `move-column-left/right` go to the neighbor monitor, or, with none, follow column order into the adjacent band. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band; `move-window-up/down` takes the same path, moving the whole column to the adjacent band by swapping it with that column. In a partial last band that column is the closest one, so a column moved down and back up may land at another place in its band. Column widths are cells within the band (see [Column widths](#column-widths)); window height shares apply too. Maximizing a column hides its same-band siblings until focus moves or maximization ends. | 230 | + | Left/right focus and moves stay in the band; at its edge, `focus-column-left/right` go first to the previous or next occupied workspace; past the first or last one, to the neighbor monitor on that side; with none there, to the trailing empty workspace; `move-column-left/right` go to the neighbor monitor, or, with none, follow column order into the adjacent band. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band; `move-window-up/down` takes the same path, moving the whole column to the adjacent band by swapping it with that column. In a partial last band that column is the closest one, so a column moved down and back up may land at another place in its band. Column widths are cells within the band (see [Column widths](#column-widths)); window height shares apply too. A maximized column takes a band of its own: the columns after it continue in the following bands, and unmaximizing packs them back. It stays maximized while focus moves. |
| 231 | 231 | ||||
| 232 | Three-finger vertical swipes scroll bands; horizontal swipes switch workspaces. Scroll and fixed workspaces retain their existing gesture axes. A switch keeps the source workspace's axis throughout its transition; the destination's axes apply to the next gesture. The `focus-workspace-prev/next` commands switch to the previous or next workspace, whether the transition is vertical or horizontal. Four-finger and stash gestures keep their existing behaviour. | 232 | Three-finger vertical swipes scroll bands; horizontal swipes switch workspaces. Scroll and fixed workspaces retain their existing gesture axes. A switch keeps the source workspace's axis throughout its transition; the destination's axes apply to the next gesture. The `focus-workspace-prev/next` commands switch to the previous or next workspace, whether the transition is vertical or horizontal. Four-finger and stash gestures keep their existing behaviour. | ||
| 233 | 233 | ||||
| 234 | - | In the [overview](#overview), the selected band is centred at the usual overview zoom, whatever the band count. The other bands of the workspace sit above and below it, dimmed, and may run off screen. Neighbor workspaces sit left and right, fully dimmed, with their selected row level with the centred band. The stash pile stays left of the band, inside the workspace's slot. A maximized column shows the columns it hides as dimmed cards behind it, peeking out on their side of the band: the nearest one on each side, then the next ones stacked at the band's edge. Selecting a card (left/right, a three-finger swipe or a click on a visible card) moves the maximization to it; Return keeps it there and Escape gives it back. | 234 | + | In the [overview](#overview), the selected band is centred at the usual overview zoom, whatever the band count. The other bands of the workspace sit above and below it, dimmed, and may run off screen. Neighbor workspaces sit left and right, fully dimmed, with their selected row level with the centred band. The stash pile stays left of the band, inside the workspace's slot. |
| 235 | 235 | ||||
| 236 | - | In the overview, up/down move to the previous or next band and stop at the first and last; with a stash card selected they browse the pile. Left/right move along the band; past its first column they enter the stash, or go to the previous workspace when there is none; past its last column they go to the next workspace (with the overview closed, focus goes to the previous or next occupied workspace at the band's edge, then to the neighbor monitor, then to the trailing empty workspace). Covering floats fan left and right of the band: left/right traverse them before entering the stash or changing workspace, and up/down do nothing on a float. `h`/`j`/`k`/`l` and the focus binds follow the arrows; `focus-workspace-prev/next` always change workspace, except over a selected stash card, where they browse the pile. Three-finger swipes follow the fingers: vertical moves one band (or one stash card) and horizontal one workspace per gesture, even over a stash card. Over a maximized column that hides cards, horizontal moves one step per gesture like left/right: the next card, then past the band's edge the stash or a workspace. A vertical wheel or continuous scroll changes workspace, except over a selected stash card, where it browses the pile; a horizontal one moves along the band. | 236 | + | In the overview, up/down move to the previous or next band and stop at the first and last; with a stash card selected they browse the pile. Left/right move along the band; past its first column they enter the stash, or go to the previous workspace when there is none; past its last column they go to the next workspace (with the overview closed, focus goes to the previous or next occupied workspace at the band's edge, then to the neighbor monitor, then to the trailing empty workspace). Covering floats fan left and right of the band: left/right traverse them before entering the stash or changing workspace, and up/down do nothing on a float. `h`/`j`/`k`/`l` and the focus binds follow the arrows; `focus-workspace-prev/next` always change workspace, except over a selected stash card, where they browse the pile. Three-finger swipes follow the fingers: vertical moves one band (or one stash card) and horizontal one workspace per gesture, even over a stash card. A vertical wheel or continuous scroll changes workspace, except over a selected stash card, where it browses the pile; a horizontal one moves along the band. |
| 237 | 237 | ||||
| 238 | Directional focus past the edge of the content switches workspace along the layout's workspace axis: up/down in scroll and fixed overflow, left/right in cascade. | 238 | Directional focus past the edge of the content switches workspace along the layout's workspace axis: up/down in scroll and fixed overflow, left/right in cascade. | ||
| 239 | 239 | ||||
| @@ -263,9 +263,9 @@ In a row, the auto columns share the cells the set ones leave; when no cell is l | |||||
| 263 | | `toggle-floating` | Make the focused tile a [free floating window](#free-floating-windows) at its current size, centred; on a floating window, send it back to its former place in the columns (a native dialog becomes a new column). Does nothing on a stashed or fullscreen window | | 263 | | `toggle-floating` | Make the focused tile a [free floating window](#free-floating-windows) at its current size, centred; on a floating window, send it back to its former place in the columns (a native dialog becomes a new column). Does nothing on a stashed or fullscreen window | | ||
| 264 | | `toggle-stash-visible` | Hide the stash and give the focus back to the tiles, or show it again with the focus on its selected window. Native dialogs stay as they are. Over a fullscreen window, shows the stash on top with the focus; the window stays fullscreen behind (composed, so no direct scanout while the stash shows) and is back when the stash hides or the focus leaves it. `toggle-window-stash` or `toggle-fullscreen` on a stashed window there leaves that fullscreen for it. An activation of a stashed window (an app asking for attention, a taskbar click) shows the stash over the fullscreen window the same way. New windows never join or show over it; dialogs of the fullscreen window or of stashed windows, and their own dialogs, show above it | | 264 | | `toggle-stash-visible` | Hide the stash and give the focus back to the tiles, or show it again with the focus on its selected window. Native dialogs stay as they are. Over a fullscreen window, shows the stash on top with the focus; the window stays fullscreen behind (composed, so no direct scanout while the stash shows) and is back when the stash hides or the focus leaves it. `toggle-window-stash` or `toggle-fullscreen` on a stashed window there leaves that fullscreen for it. An activation of a stashed window (an app asking for attention, a taskbar click) shows the stash over the fullscreen window the same way. New windows never join or show over it; dialogs of the fullscreen window or of stashed windows, and their own dialogs, show above it | | ||
| 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. Outside cascade, 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 | | ||
| 268 | - | | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off. On a window that made itself fullscreen (e.g. a Wine app at monitor size), it first returns the window to its column | | 268 | + | | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off; in cascade, the column takes a band of its own. On a window that made itself fullscreen (e.g. a Wine app at monitor size), it first returns the window to its column | |
| 269 | | `cycle-column-width` | Grow the focused column by one cell, taking it from the widest other column of its row (see [Column widths](#column-widths)). When no column has a cell to give, every column of the row goes back to equal shares. In scroll overflow, the column grows to `max-columns` cells, then back to auto. A lone column or a full row cannot grow: it goes back to equal shares if its cells were set, and otherwise nothing changes. A spiral never changes | | 269 | | `cycle-column-width` | Grow the focused column by one cell, taking it from the widest other column of its row (see [Column widths](#column-widths)). When no column has a cell to give, every column of the row goes back to equal shares. In scroll overflow, the column grows to `max-columns` cells, then back to auto. A lone column or a full row cannot grow: it goes back to equal shares if its cells were set, and otherwise nothing changes. A spiral never changes | | ||
| 270 | | `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor. In cascade overflow the edge is the band's, and past it comes the previous or next occupied workspace, then the neighbor monitor, then the trailing empty workspace. In fixed overflow, the neighbor on screen. From a fullscreen window, the neighbor column comes first and fullscreen ends; at the edge it stays | | 270 | | `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor. In cascade overflow the edge is the band's, and past it comes the previous or next occupied workspace, then the neighbor monitor, then the trailing empty workspace. In fixed overflow, the neighbor on screen. From a fullscreen window, the neighbor column comes first and fullscreen ends; at the edge it stays | | ||
| 271 | | `focus-window-up/down` | Focus in the column; past the edge, the previous or next workspace (in cascade overflow, the adjacent band, stopping at the first and last). In fixed overflow, the column on screen above or below comes first. Up at the top brings back a covering float put behind the columns. From a fullscreen window, a window above or below comes first and fullscreen ends | | 271 | | `focus-window-up/down` | Focus in the column; past the edge, the previous or next workspace (in cascade overflow, the adjacent band, stopping at the first and last). In fixed overflow, the column on screen above or below comes first. Up at the top brings back a covering float put behind the columns. From a fullscreen window, a window above or below comes first and fullscreen ends | | ||
| @@ -312,7 +312,7 @@ The drop can be on another monitor; focus follows the window. There, the middle | |||||
| 312 | 312 | ||||
| 313 | ### Overview | 313 | ### Overview | ||
| 314 | 314 | ||||
| 315 | - | `toggle-overview` shows scaled previews of the current workspace, including over fullscreen windows, over a background darkened by `floating.dim`. Windows keep their size and previews show their last frames. Covering floats and windows hidden behind a maximized column in `fixed` overflow appear as cards: the on-screen item is in front, hidden columns share one spiral-layout card, and in a vertical overview up to two cards behind peek above it and passed cards peek below (a Cascade overview fans them left and right). Neighbor workspaces show dimmed stacks. Each monitor has one overview, laid out along the navigation axis of the current workspace's layout: neighbors appear above and below a `scroll` or `fixed` workspace and left and right of a `cascade` one (see [Cascade](#cascade)); stepping to a workspace with another layout turns the overview. A named workspace appears directly below the numbered workspace from which its `workspace <name>` bind was invoked. Invoking it from another numbered workspace updates this placement; browsing the overview does not. Several named workspaces attached to the same numbered workspace follow configuration order, with rules across the axis (horizontal in a vertical overview, vertical in a side-by-side one) separating their group from numbered rows. Named-to-named binds reuse the source's numbered attachment. Unattached named workspaces follow the last occupied numbered row, or the first row if all numbered workspaces are empty. The selected workspace stays centred and its immediate neighbors appear beside it along that axis; in a vertical overview, rows and stash previews keep their full width. Empty workspaces are skipped unless currently selected or serving as the invocation anchor of a visible named row. Named workspaces remain outside the numbered list and normal previous/next workspace navigation. Smaller floats, such as dialogs, stay hidden. A fullscreen floating window, or any fullscreen window with `fixed` overflow, is its workspace's front card, with the windows it hides behind it; `scroll` overflow keeps its columns selectable in one card. Choosing the fullscreen window keeps it fullscreen; choosing another window leaves fullscreen. | 315 | + | `toggle-overview` shows scaled previews of the current workspace, including over fullscreen windows, over a background darkened by `floating.dim`. Windows keep their size and previews show their last frames. Covering floats and windows hidden behind a maximized column in `fixed` overflow appear as cards: the on-screen item is in front, hidden columns share one spiral-layout card, and in a vertical overview up to two cards behind peek above it and passed cards peek below (a Cascade overview fans covering floats left and right). Neighbor workspaces show dimmed stacks. Each monitor has one overview, laid out along the navigation axis of the current workspace's layout: neighbors appear above and below a `scroll` or `fixed` workspace and left and right of a `cascade` one (see [Cascade](#cascade)); stepping to a workspace with another layout turns the overview. A named workspace appears directly below the numbered workspace from which its `workspace <name>` bind was invoked. Invoking it from another numbered workspace updates this placement; browsing the overview does not. Several named workspaces attached to the same numbered workspace follow configuration order, with rules across the axis (horizontal in a vertical overview, vertical in a side-by-side one) separating their group from numbered rows. Named-to-named binds reuse the source's numbered attachment. Unattached named workspaces follow the last occupied numbered row, or the first row if all numbered workspaces are empty. The selected workspace stays centred and its immediate neighbors appear beside it along that axis; in a vertical overview, rows and stash previews keep their full width. Empty workspaces are skipped unless currently selected or serving as the invocation anchor of a visible named row. Named workspaces remain outside the numbered list and normal previous/next workspace navigation. Smaller floats, such as dialogs, stay hidden. A fullscreen floating window, or any fullscreen window with `fixed` overflow, is its workspace's front card, with the windows it hides behind it; `scroll` overflow keeps its columns selectable in one card. Choosing the fullscreen window keeps it fullscreen; choosing another window leaves fullscreen. |
| 316 | 316 | ||||
| 317 | | Key | Action | | 317 | | Key | Action | | ||
| 318 | | --- | --- | | 318 | | --- | --- | | ||
M internal/core/cascade.go +72 -12
| @@ -2,7 +2,63 @@ package core | |||
| 2 | 2 | ||
| 3 | 3 | import "slices" | |
| 4 | 4 | ||
| 5 | - | func (w *Workspace) band(i int) int { return i / max(w.MaxColumns, 1) } | |
| 5 | + | // Cascade bands pack columns in order: a band ends when the next column | |
| 6 | + | // would take it past MaxColumns slots. A column takes one slot and a | |
| 7 | + | // maximized one all of them, so it sits alone in its band. Cells only | |
| 8 | + | // share a band's width (laneWidth); they never move a column to a band. | |
| 9 | + | ||
| 10 | + | // bandSlots is the number of slots column i takes in its band. | |
| 11 | + | func (w *Workspace) bandSlots(i int) int { | |
| 12 | + | if w.Columns[i].FullWidth { | |
| 13 | + | return max(w.MaxColumns, 1) | |
| 14 | + | } | |
| 15 | + | return 1 | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | // walkBands calls f with each band and its column range, in order, until f | |
| 19 | + | // returns false. | |
| 20 | + | func (w *Workspace) walkBands(f func(b, first, last int) bool) { | |
| 21 | + | k := max(w.MaxColumns, 1) | |
| 22 | + | b, first, sum := 0, 0, 0 | |
| 23 | + | for i := range w.Columns { | |
| 24 | + | c := w.bandSlots(i) | |
| 25 | + | if i > first && sum+c > k { | |
| 26 | + | if !f(b, first, i-1) { | |
| 27 | + | return | |
| 28 | + | } | |
| 29 | + | b, first, sum = b+1, i, 0 | |
| 30 | + | } | |
| 31 | + | sum += c | |
| 32 | + | } | |
| 33 | + | if len(w.Columns) > 0 { | |
| 34 | + | f(b, first, len(w.Columns)-1) | |
| 35 | + | } | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // band is the cascade band of column i. | |
| 39 | + | func (w *Workspace) band(i int) (band int) { | |
| 40 | + | w.walkBands(func(b, _, last int) bool { | |
| 41 | + | band = b | |
| 42 | + | return last < i | |
| 43 | + | }) | |
| 44 | + | return band | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // bandBounds is the column range of band b, first > last past the bands. | |
| 48 | + | func (w *Workspace) bandBounds(b int) (first, last int) { | |
| 49 | + | first, last = 0, -1 | |
| 50 | + | if b < 0 { | |
| 51 | + | return first, last | |
| 52 | + | } | |
| 53 | + | w.walkBands(func(n, f, l int) bool { | |
| 54 | + | if n == b { | |
| 55 | + | first, last = f, l | |
| 56 | + | } | |
| 57 | + | return n < b | |
| 58 | + | }) | |
| 59 | + | return first, last | |
| 60 | + | } | |
| 61 | + | ||
| 6 | 62 | func (w *Workspace) sameBand(a, b int) bool { return !w.policy().wraps || w.band(a) == w.band(b) } | |
| 7 | 63 | ||
| 8 | 64 | // cellX is the x of column i in its lane, from the usable area's left edge. | |
| @@ -88,16 +144,22 @@ func (w *Workspace) equalShare(k int) int { | |||
| 88 | 144 | k = max(k, 1) | |
| 89 | 145 | return max((w.Usable.W-w.gap()*(k+1))/k, 0) | |
| 90 | 146 | } | |
| 147 | + | ||
| 148 | + | // cellInBand is the column of band b at the focus's offset in its band, | |
| 149 | + | // clamped to the band. | |
| 91 | 150 | func (w *Workspace) cellInBand(b int) int { | |
| 92 | - | k := max(w.MaxColumns, 1) | |
| 93 | - | return min(b*k+w.Focus%k, len(w.Columns)-1) | |
| 151 | + | first, _ := w.bandBounds(w.band(w.Focus)) | |
| 152 | + | nfirst, nlast := w.bandBounds(b) | |
| 153 | + | return min(nfirst+w.Focus-first, nlast) | |
| 94 | 154 | } | |
| 155 | + | ||
| 156 | + | // focusBand focuses the column in the band dir away; a maximized column | |
| 157 | + | // keeps its own band, so it stays maximized. | |
| 95 | 158 | func (w *Workspace) focusBand(dir int) bool { | |
| 96 | 159 | i := w.laneNeighbor(dir) | |
| 97 | 160 | if i < 0 { | |
| 98 | 161 | return false | |
| 99 | 162 | } | |
| 100 | - | w.unmaximize() | |
| 101 | 163 | w.Focus = i | |
| 102 | 164 | w.Columns[i].Focus = 0 | |
| 103 | 165 | if dir < 0 { | |
| @@ -120,7 +182,7 @@ func (w *Workspace) cascadeRectsInto(dst []Rect, ignoreFullWidth bool) []Rect { | |||
| 120 | 182 | x := g | |
| 121 | 183 | for i := first; i <= last; i++ { | |
| 122 | 184 | cw := w.laneWidth(i, first, last, S, u, sum) | |
| 123 | - | r := Rect{X: w.Usable.X + x, Y: w.Usable.Y + g + w.band(i)*w.Usable.H - view, W: cw, H: max(w.Usable.H-2*g, 0)} | |
| 185 | + | r := Rect{X: w.Usable.X + x, Y: w.Usable.Y + g + lane*w.Usable.H - view, W: cw, H: max(w.Usable.H-2*g, 0)} | |
| 124 | 186 | if w.Columns[i].FullWidth && !ignoreFullWidth { | |
| 125 | 187 | r.X = w.Usable.X + g | |
| 126 | 188 | r.W = max(w.Usable.W-2*g, 0) | |
| @@ -161,15 +223,13 @@ func (w *Workspace) laneOf(i int) int { | |||
| 161 | 223 | // laneBounds is the range of columns of a lane, first > last when the lane | |
| 162 | 224 | // does not exist. A layout that does not wrap has the whole row as lane 0. | |
| 163 | 225 | func (w *Workspace) laneBounds(lane int) (first, last int) { | |
| 164 | - | k := len(w.Columns) | |
| 165 | 226 | if w.policy().wraps { | |
| 166 | - | k = max(w.MaxColumns, 1) | |
| 227 | + | return w.bandBounds(lane) | |
| 167 | 228 | } | |
| 168 | - | first = lane * k | |
| 169 | - | if lane < 0 || first >= len(w.Columns) { | |
| 229 | + | if lane != 0 || len(w.Columns) == 0 { | |
| 170 | 230 | return 0, -1 | |
| 171 | 231 | } | |
| 172 | - | return first, min(first+k, len(w.Columns)) - 1 | |
| 232 | + | return 0, len(w.Columns) - 1 | |
| 173 | 233 | } | |
| 174 | 234 | ||
| 175 | 235 | // leavesBand reports whether column i, where a left/right move from the | |
| @@ -181,13 +241,13 @@ func (w *Workspace) leavesBand(i int) bool { | |||
| 181 | 241 | ||
| 182 | 242 | // moveBand swaps the focused cascade column with the column in the band dir | |
| 183 | 243 | // away where focus-window-up/down goes; focus follows it. Past the first | |
| 184 | - | // or last band nothing moves. | |
| 244 | + | // or last band nothing moves. Columns keep their maximization: bands pack | |
| 245 | + | // again around them. | |
| 185 | 246 | func (w *Workspace) moveBand(dir int) { | |
| 186 | 247 | i := w.laneNeighbor(dir) | |
| 187 | 248 | if i < 0 { | |
| 188 | 249 | return | |
| 189 | 250 | } | |
| 190 | - | w.unmaximize() | |
| 191 | 251 | w.Columns[w.Focus], w.Columns[i] = w.Columns[i], w.Columns[w.Focus] | |
| 192 | 252 | w.Focus = i | |
| 193 | 253 | w.scroll() | |
| @@ -2,7 +2,63 @@ package core | |||||
| 2 | 2 | ||||
| 3 | import "slices" | 3 | import "slices" | ||
| 4 | 4 | ||||
| 5 | - | func (w *Workspace) band(i int) int { return i / max(w.MaxColumns, 1) } | 5 | + | // Cascade bands pack columns in order: a band ends when the next column |
| 6 | + | // would take it past MaxColumns slots. A column takes one slot and a | |||
| 7 | + | // maximized one all of them, so it sits alone in its band. Cells only | |||
| 8 | + | // share a band's width (laneWidth); they never move a column to a band. | |||
| 9 | + | ||||
| 10 | + | // bandSlots is the number of slots column i takes in its band. | |||
| 11 | + | func (w *Workspace) bandSlots(i int) int { | |||
| 12 | + | if w.Columns[i].FullWidth { | |||
| 13 | + | return max(w.MaxColumns, 1) | |||
| 14 | + | } | |||
| 15 | + | return 1 | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | // walkBands calls f with each band and its column range, in order, until f | |||
| 19 | + | // returns false. | |||
| 20 | + | func (w *Workspace) walkBands(f func(b, first, last int) bool) { | |||
| 21 | + | k := max(w.MaxColumns, 1) | |||
| 22 | + | b, first, sum := 0, 0, 0 | |||
| 23 | + | for i := range w.Columns { | |||
| 24 | + | c := w.bandSlots(i) | |||
| 25 | + | if i > first && sum+c > k { | |||
| 26 | + | if !f(b, first, i-1) { | |||
| 27 | + | return | |||
| 28 | + | } | |||
| 29 | + | b, first, sum = b+1, i, 0 | |||
| 30 | + | } | |||
| 31 | + | sum += c | |||
| 32 | + | } | |||
| 33 | + | if len(w.Columns) > 0 { | |||
| 34 | + | f(b, first, len(w.Columns)-1) | |||
| 35 | + | } | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // band is the cascade band of column i. | |||
| 39 | + | func (w *Workspace) band(i int) (band int) { | |||
| 40 | + | w.walkBands(func(b, _, last int) bool { | |||
| 41 | + | band = b | |||
| 42 | + | return last < i | |||
| 43 | + | }) | |||
| 44 | + | return band | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // bandBounds is the column range of band b, first > last past the bands. | |||
| 48 | + | func (w *Workspace) bandBounds(b int) (first, last int) { | |||
| 49 | + | first, last = 0, -1 | |||
| 50 | + | if b < 0 { | |||
| 51 | + | return first, last | |||
| 52 | + | } | |||
| 53 | + | w.walkBands(func(n, f, l int) bool { | |||
| 54 | + | if n == b { | |||
| 55 | + | first, last = f, l | |||
| 56 | + | } | |||
| 57 | + | return n < b | |||
| 58 | + | }) | |||
| 59 | + | return first, last | |||
| 60 | + | } | |||
| 61 | + | ||||
| 6 | func (w *Workspace) sameBand(a, b int) bool { return !w.policy().wraps || w.band(a) == w.band(b) } | 62 | func (w *Workspace) sameBand(a, b int) bool { return !w.policy().wraps || w.band(a) == w.band(b) } | ||
| 7 | 63 | ||||
| 8 | // cellX is the x of column i in its lane, from the usable area's left edge. | 64 | // cellX is the x of column i in its lane, from the usable area's left edge. | ||
| @@ -88,16 +144,22 @@ func (w *Workspace) equalShare(k int) int { | |||||
| 88 | k = max(k, 1) | 144 | k = max(k, 1) | ||
| 89 | return max((w.Usable.W-w.gap()*(k+1))/k, 0) | 145 | return max((w.Usable.W-w.gap()*(k+1))/k, 0) | ||
| 90 | } | 146 | } | ||
| 147 | + | ||||
| 148 | + | // cellInBand is the column of band b at the focus's offset in its band, | |||
| 149 | + | // clamped to the band. | |||
| 91 | func (w *Workspace) cellInBand(b int) int { | 150 | func (w *Workspace) cellInBand(b int) int { | ||
| 92 | - | k := max(w.MaxColumns, 1) | 151 | + | first, _ := w.bandBounds(w.band(w.Focus)) |
| 93 | - | return min(b*k+w.Focus%k, len(w.Columns)-1) | 152 | + | nfirst, nlast := w.bandBounds(b) |
| 153 | + | return min(nfirst+w.Focus-first, nlast) | |||
| 94 | } | 154 | } | ||
| 155 | + | ||||
| 156 | + | // focusBand focuses the column in the band dir away; a maximized column | |||
| 157 | + | // keeps its own band, so it stays maximized. | |||
| 95 | func (w *Workspace) focusBand(dir int) bool { | 158 | func (w *Workspace) focusBand(dir int) bool { | ||
| 96 | i := w.laneNeighbor(dir) | 159 | i := w.laneNeighbor(dir) | ||
| 97 | if i < 0 { | 160 | if i < 0 { | ||
| 98 | return false | 161 | return false | ||
| 99 | } | 162 | } | ||
| 100 | - | w.unmaximize() | |||
| 101 | w.Focus = i | 163 | w.Focus = i | ||
| 102 | w.Columns[i].Focus = 0 | 164 | w.Columns[i].Focus = 0 | ||
| 103 | if dir < 0 { | 165 | if dir < 0 { | ||
| @@ -120,7 +182,7 @@ func (w *Workspace) cascadeRectsInto(dst []Rect, ignoreFullWidth bool) []Rect { | |||||
| 120 | x := g | 182 | x := g | ||
| 121 | for i := first; i <= last; i++ { | 183 | for i := first; i <= last; i++ { | ||
| 122 | cw := w.laneWidth(i, first, last, S, u, sum) | 184 | cw := w.laneWidth(i, first, last, S, u, sum) | ||
| 123 | - | r := Rect{X: w.Usable.X + x, Y: w.Usable.Y + g + w.band(i)*w.Usable.H - view, W: cw, H: max(w.Usable.H-2*g, 0)} | 185 | + | r := Rect{X: w.Usable.X + x, Y: w.Usable.Y + g + lane*w.Usable.H - view, W: cw, H: max(w.Usable.H-2*g, 0)} |
| 124 | if w.Columns[i].FullWidth && !ignoreFullWidth { | 186 | if w.Columns[i].FullWidth && !ignoreFullWidth { | ||
| 125 | r.X = w.Usable.X + g | 187 | r.X = w.Usable.X + g | ||
| 126 | r.W = max(w.Usable.W-2*g, 0) | 188 | r.W = max(w.Usable.W-2*g, 0) | ||
| @@ -161,15 +223,13 @@ func (w *Workspace) laneOf(i int) int { | |||||
| 161 | // laneBounds is the range of columns of a lane, first > last when the lane | 223 | // laneBounds is the range of columns of a lane, first > last when the lane | ||
| 162 | // does not exist. A layout that does not wrap has the whole row as lane 0. | 224 | // does not exist. A layout that does not wrap has the whole row as lane 0. | ||
| 163 | func (w *Workspace) laneBounds(lane int) (first, last int) { | 225 | func (w *Workspace) laneBounds(lane int) (first, last int) { | ||
| 164 | - | k := len(w.Columns) | |||
| 165 | if w.policy().wraps { | 226 | if w.policy().wraps { | ||
| 166 | - | k = max(w.MaxColumns, 1) | 227 | + | return w.bandBounds(lane) |
| 167 | } | 228 | } | ||
| 168 | - | first = lane * k | 229 | + | if lane != 0 || len(w.Columns) == 0 { |
| 169 | - | if lane < 0 || first >= len(w.Columns) { | |||
| 170 | return 0, -1 | 230 | return 0, -1 | ||
| 171 | } | 231 | } | ||
| 172 | - | return first, min(first+k, len(w.Columns)) - 1 | 232 | + | return 0, len(w.Columns) - 1 |
| 173 | } | 233 | } | ||
| 174 | 234 | ||||
| 175 | // leavesBand reports whether column i, where a left/right move from the | 235 | // leavesBand reports whether column i, where a left/right move from the | ||
| @@ -181,13 +241,13 @@ func (w *Workspace) leavesBand(i int) bool { | |||||
| 181 | 241 | ||||
| 182 | // moveBand swaps the focused cascade column with the column in the band dir | 242 | // moveBand swaps the focused cascade column with the column in the band dir | ||
| 183 | // away where focus-window-up/down goes; focus follows it. Past the first | 243 | // away where focus-window-up/down goes; focus follows it. Past the first | ||
| 184 | - | // or last band nothing moves. | 244 | + | // or last band nothing moves. Columns keep their maximization: bands pack |
| 245 | + | // again around them. | |||
| 185 | func (w *Workspace) moveBand(dir int) { | 246 | func (w *Workspace) moveBand(dir int) { | ||
| 186 | i := w.laneNeighbor(dir) | 247 | i := w.laneNeighbor(dir) | ||
| 187 | if i < 0 { | 248 | if i < 0 { | ||
| 188 | return | 249 | return | ||
| 189 | } | 250 | } | ||
| 190 | - | w.unmaximize() | |||
| 191 | w.Columns[w.Focus], w.Columns[i] = w.Columns[i], w.Columns[w.Focus] | 251 | w.Columns[w.Focus], w.Columns[i] = w.Columns[i], w.Columns[w.Focus] | ||
| 192 | w.Focus = i | 252 | w.Focus = i | ||
| 193 | w.scroll() | 253 | w.scroll() | ||
M internal/core/cascade_moves_test.go +8 -7
| @@ -188,16 +188,17 @@ func TestCascadeMoveWindowStopsAtOuterBands(t *testing.T) { | |||
| 188 | 188 | wantFocus(t, c, "M", 4) | |
| 189 | 189 | } | |
| 190 | 190 | ||
| 191 | - | // A maximized column leaving its band gives up maximization: the band it | |
| 192 | - | // joins shows all its columns. | |
| 193 | - | func TestCascadeMoveWindowEndsMaximization(t *testing.T) { | |
| 191 | + | // A maximized column keeps its band to itself when it moves: it swaps | |
| 192 | + | // with the column of the band above and stays maximized. | |
| 193 | + | func TestCascadeMoveWindowKeepsMaximization(t *testing.T) { | |
| 194 | 194 | c, w := cascadeMovesCore(t, 5, 3, false) | |
| 195 | 195 | w.FocusID(5) | |
| 196 | - | w.ToggleFullWidth() | |
| 196 | + | w.ToggleFullWidth() // Bands {1 2 3} {4} {5}. | |
| 197 | 197 | c.applyAction(ActionMoveWindowUp) | |
| 198 | - | wantColumns(t, w, 1, 5, 3, 4, 2) | |
| 199 | - | if w.Columns[1].FullWidth || len(w.maximized) != 0 { | |
| 200 | - | t.Fatalf("still maximized: full width %t, MRU %v", w.Columns[1].FullWidth, w.maximized) | |
| 198 | + | wantColumns(t, w, 1, 2, 3, 5, 4) | |
| 199 | + | wantFocus(t, c, "M", 5) | |
| 200 | + | if !w.Columns[3].FullWidth || w.band(3) != 1 || w.band(4) != 2 { | |
| 201 | + | t.Fatalf("maximized %t, bands %d %d", w.Columns[3].FullWidth, w.band(3), w.band(4)) | |
| 201 | 202 | } | |
| 202 | 203 | } | |
| 203 | 204 | ||
| @@ -188,16 +188,17 @@ func TestCascadeMoveWindowStopsAtOuterBands(t *testing.T) { | |||||
| 188 | wantFocus(t, c, "M", 4) | 188 | wantFocus(t, c, "M", 4) | ||
| 189 | } | 189 | } | ||
| 190 | 190 | ||||
| 191 | - | // A maximized column leaving its band gives up maximization: the band it | 191 | + | // A maximized column keeps its band to itself when it moves: it swaps |
| 192 | - | // joins shows all its columns. | 192 | + | // with the column of the band above and stays maximized. |
| 193 | - | func TestCascadeMoveWindowEndsMaximization(t *testing.T) { | 193 | + | func TestCascadeMoveWindowKeepsMaximization(t *testing.T) { |
| 194 | c, w := cascadeMovesCore(t, 5, 3, false) | 194 | c, w := cascadeMovesCore(t, 5, 3, false) | ||
| 195 | w.FocusID(5) | 195 | w.FocusID(5) | ||
| 196 | - | w.ToggleFullWidth() | 196 | + | w.ToggleFullWidth() // Bands {1 2 3} {4} {5}. |
| 197 | c.applyAction(ActionMoveWindowUp) | 197 | c.applyAction(ActionMoveWindowUp) | ||
| 198 | - | wantColumns(t, w, 1, 5, 3, 4, 2) | 198 | + | wantColumns(t, w, 1, 2, 3, 5, 4) |
| 199 | - | if w.Columns[1].FullWidth || len(w.maximized) != 0 { | 199 | + | wantFocus(t, c, "M", 5) |
| 200 | - | t.Fatalf("still maximized: full width %t, MRU %v", w.Columns[1].FullWidth, w.maximized) | 200 | + | if !w.Columns[3].FullWidth || w.band(3) != 1 || w.band(4) != 2 { |
| 201 | + | t.Fatalf("maximized %t, bands %d %d", w.Columns[3].FullWidth, w.band(3), w.band(4)) | |||
| 201 | } | 202 | } | ||
| 202 | } | 203 | } | ||
| 203 | 204 | ||||
M internal/core/cascade_test.go +42 -17
| @@ -142,20 +142,48 @@ func TestCascadeCompactionAndNavigation(t *testing.T) { | |||
| 142 | 142 | } | |
| 143 | 143 | } | |
| 144 | 144 | ||
| 145 | - | func TestCascadeCovers(t *testing.T) { | |
| 146 | - | w := cascadeWorkspace(4) | |
| 145 | + | // A maximized column takes a band of its own: the columns after it pack | |
| 146 | + | // into the next bands, and unmaximizing packs them back. | |
| 147 | + | func TestCascadeMaximizedColumnOwnsBand(t *testing.T) { | |
| 148 | + | w := cascadeWorkspace(4) // Bands {1 2 3} {4}. | |
| 147 | 149 | w.FocusID(2) | |
| 148 | 150 | w.ToggleFullWidth() | |
| 149 | - | ps := w.Layout() | |
| 150 | - | for _, p := range ps { | |
| 151 | - | if p.ID == 2 && p.Rect.W != 900 { | |
| 151 | + | bands := func() []int { | |
| 152 | + | var b []int | |
| 153 | + | for i := range w.Columns { | |
| 154 | + | b = append(b, w.band(i)) | |
| 155 | + | } | |
| 156 | + | return b | |
| 157 | + | } | |
| 158 | + | if got := bands(); !slices.Equal(got, []int{0, 1, 2, 2}) { | |
| 159 | + | t.Fatalf("bands %v, want [0 1 2 2]", got) | |
| 160 | + | } | |
| 161 | + | if w.View != 600 { | |
| 162 | + | t.Fatalf("view %d, want the maximized band", w.View) | |
| 163 | + | } | |
| 164 | + | for _, p := range w.Layout() { | |
| 165 | + | if p.ID == 2 && (p.Rect.W != 900 || p.Rect.Y != 0) { | |
| 152 | 166 | t.Fatalf("full width=%v", p.Rect) | |
| 153 | 167 | } | |
| 154 | - | if (p.ID == 1 || p.ID == 3) && !p.Hidden { | |
| 155 | - | t.Fatalf("sibling %d visible", p.ID) | |
| 168 | + | if p.ID != 2 && p.Rect.Y == 0 && !p.Hidden { | |
| 169 | + | t.Fatalf("column %d shares the maximized band: %v", p.ID, p.Rect) | |
| 156 | 170 | } | |
| 157 | 171 | } | |
| 172 | + | // Focus moves between bands without unmaximizing. | |
| 173 | + | w.FocusID(1) | |
| 174 | + | if !w.Columns[1].FullWidth || w.View != 0 { | |
| 175 | + | t.Fatalf("focus left the band: maximized %t view %d", w.Columns[1].FullWidth, w.View) | |
| 176 | + | } | |
| 177 | + | w.FocusID(2) | |
| 158 | 178 | w.ToggleFullWidth() | |
| 179 | + | if got := bands(); !slices.Equal(got, []int{0, 0, 0, 1}) { | |
| 180 | + | t.Fatalf("bands after unmaximize %v", got) | |
| 181 | + | } | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | func TestCascadeCovers(t *testing.T) { | |
| 185 | + | w := cascadeWorkspace(4) | |
| 186 | + | w.FocusID(2) | |
| 159 | 187 | w.ToggleFullscreen() | |
| 160 | 188 | for _, p := range w.Layout() { | |
| 161 | 189 | if p.ID == 2 && p.Rect != w.Output { | |
| @@ -172,7 +200,8 @@ func TestCascadeCovers(t *testing.T) { | |||
| 172 | 200 | } | |
| 173 | 201 | } | |
| 174 | 202 | ||
| 175 | - | func TestCascadeClearsMaximizationOnBandChanges(t *testing.T) { | |
| 203 | + | // Band changes keep a maximized column maximized: it has its own band. | |
| 204 | + | func TestCascadeKeepsMaximizationOnBandChanges(t *testing.T) { | |
| 176 | 205 | for _, move := range []struct { | |
| 177 | 206 | name string | |
| 178 | 207 | run func(*Workspace) | |
| @@ -186,24 +215,20 @@ func TestCascadeClearsMaximizationOnBandChanges(t *testing.T) { | |||
| 186 | 215 | w.FocusID(1) | |
| 187 | 216 | w.ToggleFullWidth() | |
| 188 | 217 | move.run(w) | |
| 189 | - | for _, col := range w.Columns { | |
| 190 | - | if col.FullWidth { | |
| 191 | - | t.Fatal("stale maximization") | |
| 192 | - | } | |
| 218 | + | if i := w.columnOf(1); !w.Columns[i].FullWidth || slices.ContainsFunc(w.Columns, func(c Column) bool { return c.FullWidth && c.Windows[0] != 1 }) { | |
| 219 | + | t.Fatalf("maximization lost or moved: %+v", w.Columns) | |
| 193 | 220 | } | |
| 194 | 221 | }) | |
| 195 | 222 | } | |
| 196 | 223 | } | |
| 197 | 224 | ||
| 198 | - | func TestCascadeReloadAndStashRestoreClearMaximization(t *testing.T) { | |
| 225 | + | func TestCascadeStashRestoreKeepsReloadClearsMaximization(t *testing.T) { | |
| 199 | 226 | w := cascadeWorkspace(3) | |
| 200 | 227 | w.FocusID(1) | |
| 201 | 228 | w.ToggleFullWidth() | |
| 202 | 229 | w.restore(4, &origPlace{stacked: []WindowID{2}, col: 1}) | |
| 203 | - | for _, c := range w.Columns { | |
| 204 | - | if c.FullWidth { | |
| 205 | - | t.Fatal("restore retained maximization") | |
| 206 | - | } | |
| 230 | + | if !w.Columns[0].FullWidth { | |
| 231 | + | t.Fatal("restore into another band dropped the maximization") | |
| 207 | 232 | } | |
| 208 | 233 | m := NewMonitor() | |
| 209 | 234 | m.SetOutput(900, 600) | |
| @@ -142,20 +142,48 @@ func TestCascadeCompactionAndNavigation(t *testing.T) { | |||||
| 142 | } | 142 | } | ||
| 143 | } | 143 | } | ||
| 144 | 144 | ||||
| 145 | - | func TestCascadeCovers(t *testing.T) { | 145 | + | // A maximized column takes a band of its own: the columns after it pack |
| 146 | - | w := cascadeWorkspace(4) | 146 | + | // into the next bands, and unmaximizing packs them back. |
| 147 | + | func TestCascadeMaximizedColumnOwnsBand(t *testing.T) { | |||
| 148 | + | w := cascadeWorkspace(4) // Bands {1 2 3} {4}. | |||
| 147 | w.FocusID(2) | 149 | w.FocusID(2) | ||
| 148 | w.ToggleFullWidth() | 150 | w.ToggleFullWidth() | ||
| 149 | - | ps := w.Layout() | 151 | + | bands := func() []int { |
| 150 | - | for _, p := range ps { | 152 | + | var b []int |
| 151 | - | if p.ID == 2 && p.Rect.W != 900 { | 153 | + | for i := range w.Columns { |
| 154 | + | b = append(b, w.band(i)) | |||
| 155 | + | } | |||
| 156 | + | return b | |||
| 157 | + | } | |||
| 158 | + | if got := bands(); !slices.Equal(got, []int{0, 1, 2, 2}) { | |||
| 159 | + | t.Fatalf("bands %v, want [0 1 2 2]", got) | |||
| 160 | + | } | |||
| 161 | + | if w.View != 600 { | |||
| 162 | + | t.Fatalf("view %d, want the maximized band", w.View) | |||
| 163 | + | } | |||
| 164 | + | for _, p := range w.Layout() { | |||
| 165 | + | if p.ID == 2 && (p.Rect.W != 900 || p.Rect.Y != 0) { | |||
| 152 | t.Fatalf("full width=%v", p.Rect) | 166 | t.Fatalf("full width=%v", p.Rect) | ||
| 153 | } | 167 | } | ||
| 154 | - | if (p.ID == 1 || p.ID == 3) && !p.Hidden { | 168 | + | if p.ID != 2 && p.Rect.Y == 0 && !p.Hidden { |
| 155 | - | t.Fatalf("sibling %d visible", p.ID) | 169 | + | t.Fatalf("column %d shares the maximized band: %v", p.ID, p.Rect) |
| 156 | } | 170 | } | ||
| 157 | } | 171 | } | ||
| 172 | + | // Focus moves between bands without unmaximizing. | |||
| 173 | + | w.FocusID(1) | |||
| 174 | + | if !w.Columns[1].FullWidth || w.View != 0 { | |||
| 175 | + | t.Fatalf("focus left the band: maximized %t view %d", w.Columns[1].FullWidth, w.View) | |||
| 176 | + | } | |||
| 177 | + | w.FocusID(2) | |||
| 158 | w.ToggleFullWidth() | 178 | w.ToggleFullWidth() | ||
| 179 | + | if got := bands(); !slices.Equal(got, []int{0, 0, 0, 1}) { | |||
| 180 | + | t.Fatalf("bands after unmaximize %v", got) | |||
| 181 | + | } | |||
| 182 | + | } | |||
| 183 | + | ||||
| 184 | + | func TestCascadeCovers(t *testing.T) { | |||
| 185 | + | w := cascadeWorkspace(4) | |||
| 186 | + | w.FocusID(2) | |||
| 159 | w.ToggleFullscreen() | 187 | w.ToggleFullscreen() | ||
| 160 | for _, p := range w.Layout() { | 188 | for _, p := range w.Layout() { | ||
| 161 | if p.ID == 2 && p.Rect != w.Output { | 189 | if p.ID == 2 && p.Rect != w.Output { | ||
| @@ -172,7 +200,8 @@ func TestCascadeCovers(t *testing.T) { | |||||
| 172 | } | 200 | } | ||
| 173 | } | 201 | } | ||
| 174 | 202 | ||||
| 175 | - | func TestCascadeClearsMaximizationOnBandChanges(t *testing.T) { | 203 | + | // Band changes keep a maximized column maximized: it has its own band. |
| 204 | + | func TestCascadeKeepsMaximizationOnBandChanges(t *testing.T) { | |||
| 176 | for _, move := range []struct { | 205 | for _, move := range []struct { | ||
| 177 | name string | 206 | name string | ||
| 178 | run func(*Workspace) | 207 | run func(*Workspace) | ||
| @@ -186,24 +215,20 @@ func TestCascadeClearsMaximizationOnBandChanges(t *testing.T) { | |||||
| 186 | w.FocusID(1) | 215 | w.FocusID(1) | ||
| 187 | w.ToggleFullWidth() | 216 | w.ToggleFullWidth() | ||
| 188 | move.run(w) | 217 | move.run(w) | ||
| 189 | - | for _, col := range w.Columns { | 218 | + | if i := w.columnOf(1); !w.Columns[i].FullWidth || slices.ContainsFunc(w.Columns, func(c Column) bool { return c.FullWidth && c.Windows[0] != 1 }) { |
| 190 | - | if col.FullWidth { | 219 | + | t.Fatalf("maximization lost or moved: %+v", w.Columns) |
| 191 | - | t.Fatal("stale maximization") | |||
| 192 | - | } | |||
| 193 | } | 220 | } | ||
| 194 | }) | 221 | }) | ||
| 195 | } | 222 | } | ||
| 196 | } | 223 | } | ||
| 197 | 224 | ||||
| 198 | - | func TestCascadeReloadAndStashRestoreClearMaximization(t *testing.T) { | 225 | + | func TestCascadeStashRestoreKeepsReloadClearsMaximization(t *testing.T) { |
| 199 | w := cascadeWorkspace(3) | 226 | w := cascadeWorkspace(3) | ||
| 200 | w.FocusID(1) | 227 | w.FocusID(1) | ||
| 201 | w.ToggleFullWidth() | 228 | w.ToggleFullWidth() | ||
| 202 | w.restore(4, &origPlace{stacked: []WindowID{2}, col: 1}) | 229 | w.restore(4, &origPlace{stacked: []WindowID{2}, col: 1}) | ||
| 203 | - | for _, c := range w.Columns { | 230 | + | if !w.Columns[0].FullWidth { |
| 204 | - | if c.FullWidth { | 231 | + | t.Fatal("restore into another band dropped the maximization") |
| 205 | - | t.Fatal("restore retained maximization") | |||
| 206 | - | } | |||
| 207 | } | 232 | } | ||
| 208 | m := NewMonitor() | 233 | m := NewMonitor() | ||
| 209 | m.SetOutput(900, 600) | 234 | m.SetOutput(900, 600) | ||
M internal/core/gesture.go +1 -1
| @@ -311,7 +311,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||
| 311 | 311 | if !e.Cancelled { | |
| 312 | 312 | view := int(math.Round(target)) | |
| 313 | 313 | focus := w.snapFocus(view, target >= shown) | |
| 314 | - | if focus != w.Focus && w.policy().equalCells { | |
| 314 | + | if focus != w.Focus && w.policy().maximizeHides() { | |
| 315 | 315 | w.unmaximize() | |
| 316 | 316 | } | |
| 317 | 317 | w.Focus = focus | |
| @@ -311,7 +311,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||||
| 311 | if !e.Cancelled { | 311 | if !e.Cancelled { | ||
| 312 | view := int(math.Round(target)) | 312 | view := int(math.Round(target)) | ||
| 313 | focus := w.snapFocus(view, target >= shown) | 313 | focus := w.snapFocus(view, target >= shown) | ||
| 314 | - | if focus != w.Focus && w.policy().equalCells { | 314 | + | if focus != w.Focus && w.policy().maximizeHides() { |
| 315 | w.unmaximize() | 315 | w.unmaximize() | ||
| 316 | } | 316 | } | ||
| 317 | w.Focus = focus | 317 | w.Focus = focus | ||
M internal/core/layout.go +20 -15
| @@ -536,7 +536,7 @@ func (w *Workspace) addColumn(col Column) { | |||
| 536 | 536 | } else if len(w.Columns) > 0 { | |
| 537 | 537 | at = w.Focus + 1 | |
| 538 | 538 | } | |
| 539 | - | if w.policy().equalCells && len(w.Columns) > 0 { | |
| 539 | + | if w.policy().maximizeHides() && len(w.Columns) > 0 { | |
| 540 | 540 | w.unmaximize() | |
| 541 | 541 | } | |
| 542 | 542 | w.Columns = append(w.Columns, Column{}) | |
| @@ -688,7 +688,7 @@ func (w *Workspace) FocusID(id WindowID) bool { | |||
| 688 | 688 | w.dropStashOver() | |
| 689 | 689 | w.floatFocus, w.stashFocus = false, false | |
| 690 | 690 | w.raiseColumns() | |
| 691 | - | if w.policy().equalCells && w.Focus != i && w.Focus < len(w.Columns) && !w.Columns[i].FullWidth { | |
| 691 | + | if w.policy().maximizeHides() && w.Focus != i && w.Focus < len(w.Columns) && !w.Columns[i].FullWidth { | |
| 692 | 692 | w.unmaximize() | |
| 693 | 693 | } | |
| 694 | 694 | w.Focus = i | |
| @@ -736,7 +736,7 @@ func (w *Workspace) focusColumn(dir int) { | |||
| 736 | 736 | } | |
| 737 | 737 | if i := w.columnToward(dir); i >= 0 { | |
| 738 | 738 | w.raiseColumns() | |
| 739 | - | if w.policy().equalCells { | |
| 739 | + | if w.policy().maximizeHides() { | |
| 740 | 740 | w.unmaximize() | |
| 741 | 741 | } | |
| 742 | 742 | w.Focus = i | |
| @@ -999,7 +999,7 @@ func (w *Workspace) takeColumn() (Column, bool) { | |||
| 999 | 999 | // insertColumn adds a column at index at (clamped) and focuses it. | |
| 1000 | 1000 | func (w *Workspace) insertColumn(at int, col Column) { | |
| 1001 | 1001 | at = min(max(at, 0), len(w.Columns)) | |
| 1002 | - | if w.policy().equalCells && len(w.Columns) > 0 { | |
| 1002 | + | if w.policy().maximizeHides() && len(w.Columns) > 0 { | |
| 1003 | 1003 | w.unmaximize() | |
| 1004 | 1004 | } | |
| 1005 | 1005 | w.Columns = slices.Insert(w.Columns, at, col) | |
| @@ -1182,23 +1182,28 @@ func (w *Workspace) maximize(i int) { | |||
| 1182 | 1182 | } | |
| 1183 | 1183 | ||
| 1184 | 1184 | // hiddenByMaximized reports whether the maximized focused column hides | |
| 1185 | - | // column i on screen: in equal-cell layouts it hides the other columns of | |
| 1186 | - | // its band (the whole row in fixed overflow). | |
| 1185 | + | // column i on screen: in fixed overflow it hides the rest of the row. A | |
| 1186 | + | // maximized cascade column has its band to itself, so it hides nothing. | |
| 1187 | 1187 | func (w *Workspace) hiddenByMaximized(i int) bool { | |
| 1188 | - | return w.policy().equalCells && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth && i != w.Focus && w.sameBand(i, w.Focus) | |
| 1188 | + | return w.policy().maximizeHides() && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth && i != w.Focus | |
| 1189 | 1189 | } | |
| 1190 | 1190 | ||
| 1191 | - | // unmaximize clears the focused column in scroll mode, or all maximized | |
| 1192 | - | // columns in fixed mode (including after Escape changed focus). | |
| 1191 | + | // unmaximize clears the focused column in scroll and cascade, or all | |
| 1192 | + | // maximized columns in fixed overflow (including after Escape changed focus). | |
| 1193 | 1193 | func (w *Workspace) unmaximize() { | |
| 1194 | - | for i := range w.Columns { | |
| 1195 | - | if w.policy().equalCells || i == w.Focus { | |
| 1196 | - | w.Columns[i].FullWidth = false | |
| 1197 | - | } | |
| 1194 | + | if w.policy().maximizeHides() { | |
| 1195 | + | w.clearMaximized() | |
| 1196 | + | } else if w.Focus < len(w.Columns) { | |
| 1197 | + | w.Columns[w.Focus].FullWidth = false | |
| 1198 | 1198 | } | |
| 1199 | - | if w.policy().equalCells { | |
| 1200 | - | w.maximized = nil | |
| 1199 | + | } | |
| 1200 | + | ||
| 1201 | + | // clearMaximized unmaximizes every column and forgets the history. | |
| 1202 | + | func (w *Workspace) clearMaximized() { | |
| 1203 | + | for i := range w.Columns { | |
| 1204 | + | w.Columns[i].FullWidth = false | |
| 1201 | 1205 | } | |
| 1206 | + | w.maximized = nil | |
| 1202 | 1207 | } | |
| 1203 | 1208 | ||
| 1204 | 1209 | // transferMaximization changes the maximized column without forgetting MRU. | |
| @@ -536,7 +536,7 @@ func (w *Workspace) addColumn(col Column) { | |||||
| 536 | } else if len(w.Columns) > 0 { | 536 | } else if len(w.Columns) > 0 { | ||
| 537 | at = w.Focus + 1 | 537 | at = w.Focus + 1 | ||
| 538 | } | 538 | } | ||
| 539 | - | if w.policy().equalCells && len(w.Columns) > 0 { | 539 | + | if w.policy().maximizeHides() && len(w.Columns) > 0 { |
| 540 | w.unmaximize() | 540 | w.unmaximize() | ||
| 541 | } | 541 | } | ||
| 542 | w.Columns = append(w.Columns, Column{}) | 542 | w.Columns = append(w.Columns, Column{}) | ||
| @@ -688,7 +688,7 @@ func (w *Workspace) FocusID(id WindowID) bool { | |||||
| 688 | w.dropStashOver() | 688 | w.dropStashOver() | ||
| 689 | w.floatFocus, w.stashFocus = false, false | 689 | w.floatFocus, w.stashFocus = false, false | ||
| 690 | w.raiseColumns() | 690 | w.raiseColumns() | ||
| 691 | - | if w.policy().equalCells && w.Focus != i && w.Focus < len(w.Columns) && !w.Columns[i].FullWidth { | 691 | + | if w.policy().maximizeHides() && w.Focus != i && w.Focus < len(w.Columns) && !w.Columns[i].FullWidth { |
| 692 | w.unmaximize() | 692 | w.unmaximize() | ||
| 693 | } | 693 | } | ||
| 694 | w.Focus = i | 694 | w.Focus = i | ||
| @@ -736,7 +736,7 @@ func (w *Workspace) focusColumn(dir int) { | |||||
| 736 | } | 736 | } | ||
| 737 | if i := w.columnToward(dir); i >= 0 { | 737 | if i := w.columnToward(dir); i >= 0 { | ||
| 738 | w.raiseColumns() | 738 | w.raiseColumns() | ||
| 739 | - | if w.policy().equalCells { | 739 | + | if w.policy().maximizeHides() { |
| 740 | w.unmaximize() | 740 | w.unmaximize() | ||
| 741 | } | 741 | } | ||
| 742 | w.Focus = i | 742 | w.Focus = i | ||
| @@ -999,7 +999,7 @@ func (w *Workspace) takeColumn() (Column, bool) { | |||||
| 999 | // insertColumn adds a column at index at (clamped) and focuses it. | 999 | // insertColumn adds a column at index at (clamped) and focuses it. | ||
| 1000 | func (w *Workspace) insertColumn(at int, col Column) { | 1000 | func (w *Workspace) insertColumn(at int, col Column) { | ||
| 1001 | at = min(max(at, 0), len(w.Columns)) | 1001 | at = min(max(at, 0), len(w.Columns)) | ||
| 1002 | - | if w.policy().equalCells && len(w.Columns) > 0 { | 1002 | + | if w.policy().maximizeHides() && len(w.Columns) > 0 { |
| 1003 | w.unmaximize() | 1003 | w.unmaximize() | ||
| 1004 | } | 1004 | } | ||
| 1005 | w.Columns = slices.Insert(w.Columns, at, col) | 1005 | w.Columns = slices.Insert(w.Columns, at, col) | ||
| @@ -1182,23 +1182,28 @@ func (w *Workspace) maximize(i int) { | |||||
| 1182 | } | 1182 | } | ||
| 1183 | 1183 | ||||
| 1184 | // hiddenByMaximized reports whether the maximized focused column hides | 1184 | // hiddenByMaximized reports whether the maximized focused column hides | ||
| 1185 | - | // column i on screen: in equal-cell layouts it hides the other columns of | 1185 | + | // column i on screen: in fixed overflow it hides the rest of the row. A |
| 1186 | - | // its band (the whole row in fixed overflow). | 1186 | + | // maximized cascade column has its band to itself, so it hides nothing. |
| 1187 | func (w *Workspace) hiddenByMaximized(i int) bool { | 1187 | func (w *Workspace) hiddenByMaximized(i int) bool { | ||
| 1188 | - | return w.policy().equalCells && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth && i != w.Focus && w.sameBand(i, w.Focus) | 1188 | + | return w.policy().maximizeHides() && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth && i != w.Focus |
| 1189 | } | 1189 | } | ||
| 1190 | 1190 | ||||
| 1191 | - | // unmaximize clears the focused column in scroll mode, or all maximized | 1191 | + | // unmaximize clears the focused column in scroll and cascade, or all |
| 1192 | - | // columns in fixed mode (including after Escape changed focus). | 1192 | + | // maximized columns in fixed overflow (including after Escape changed focus). |
| 1193 | func (w *Workspace) unmaximize() { | 1193 | func (w *Workspace) unmaximize() { | ||
| 1194 | - | for i := range w.Columns { | 1194 | + | if w.policy().maximizeHides() { |
| 1195 | - | if w.policy().equalCells || i == w.Focus { | 1195 | + | w.clearMaximized() |
| 1196 | - | w.Columns[i].FullWidth = false | 1196 | + | } else if w.Focus < len(w.Columns) { |
| 1197 | - | } | 1197 | + | w.Columns[w.Focus].FullWidth = false |
| 1198 | } | 1198 | } | ||
| 1199 | - | if w.policy().equalCells { | 1199 | + | } |
| 1200 | - | w.maximized = nil | 1200 | + | |
| 1201 | + | // clearMaximized unmaximizes every column and forgets the history. | |||
| 1202 | + | func (w *Workspace) clearMaximized() { | |||
| 1203 | + | for i := range w.Columns { | |||
| 1204 | + | w.Columns[i].FullWidth = false | |||
| 1201 | } | 1205 | } | ||
| 1206 | + | w.maximized = nil | |||
| 1202 | } | 1207 | } | ||
| 1203 | 1208 | ||||
| 1204 | // transferMaximization changes the maximized column without forgetting MRU. | 1209 | // transferMaximization changes the maximized column without forgetting MRU. | ||
M internal/core/layout_policy.go +6 -0
| @@ -29,6 +29,12 @@ type layoutPolicy struct { | |||
| 29 | 29 | equalCells, wraps bool | |
| 30 | 30 | } | |
| 31 | 31 | ||
| 32 | + | // maximizeHides reports whether a maximized column hides the others, so | |
| 33 | + | // it is a transient state that focus changes undo: fixed overflow. In | |
| 34 | + | // scroll overflow and cascade it is a lasting state of its column (a | |
| 35 | + | // cascade band packs a maximized column alone). | |
| 36 | + | func (p layoutPolicy) maximizeHides() bool { return p.equalCells && !p.wraps } | |
| 37 | + | ||
| 32 | 38 | // workspaceStep is the workspace step a directional focus past the content | |
| 33 | 39 | // edge takes along axis: dir when axis is the workspace axis, else 0. | |
| 34 | 40 | func (p layoutPolicy) workspaceStep(axis layoutAxis, dir int) int { | |
| @@ -29,6 +29,12 @@ type layoutPolicy struct { | |||||
| 29 | equalCells, wraps bool | 29 | equalCells, wraps bool | ||
| 30 | } | 30 | } | ||
| 31 | 31 | ||||
| 32 | + | // maximizeHides reports whether a maximized column hides the others, so | |||
| 33 | + | // it is a transient state that focus changes undo: fixed overflow. In | |||
| 34 | + | // scroll overflow and cascade it is a lasting state of its column (a | |||
| 35 | + | // cascade band packs a maximized column alone). | |||
| 36 | + | func (p layoutPolicy) maximizeHides() bool { return p.equalCells && !p.wraps } | |||
| 37 | + | ||||
| 32 | // workspaceStep is the workspace step a directional focus past the content | 38 | // workspaceStep is the workspace step a directional focus past the content | ||
| 33 | // edge takes along axis: dir when axis is the workspace axis, else 0. | 39 | // edge takes along axis: dir when axis is the workspace axis, else 0. | ||
| 34 | func (p layoutPolicy) workspaceStep(axis layoutAxis, dir int) int { | 40 | func (p layoutPolicy) workspaceStep(axis layoutAxis, dir int) int { | ||
M internal/core/monitor.go +1 -1
| @@ -558,7 +558,7 @@ func (m *Monitor) applyNamed() { | |||
| 558 | 558 | w.Overflow = s.Overflow | |
| 559 | 559 | } | |
| 560 | 560 | if before != w.Overflow && w.policy().equalCells { | |
| 561 | - | w.unmaximize() | |
| 561 | + | w.clearMaximized() | |
| 562 | 562 | } | |
| 563 | 563 | // The view offset belongs to the old content axis: SetMaxColumns | |
| 564 | 564 | // scrolls back to the focus along the new one. | |
| @@ -558,7 +558,7 @@ func (m *Monitor) applyNamed() { | |||||
| 558 | w.Overflow = s.Overflow | 558 | w.Overflow = s.Overflow | ||
| 559 | } | 559 | } | ||
| 560 | if before != w.Overflow && w.policy().equalCells { | 560 | if before != w.Overflow && w.policy().equalCells { | ||
| 561 | - | w.unmaximize() | 561 | + | w.clearMaximized() |
| 562 | } | 562 | } | ||
| 563 | // The view offset belongs to the old content axis: SetMaxColumns | 563 | // The view offset belongs to the old content axis: SetMaxColumns | ||
| 564 | // scrolls back to the focus along the new one. | 564 | // scrolls back to the focus along the new one. | ||
M internal/core/overview.go +10 -85
| @@ -302,7 +302,7 @@ func restoreRow(w *Workspace, s rowSnapshot) { | |||
| 302 | 302 | // Fixed overflow maximizes at most one column: only the one holding | |
| 303 | 303 | // the anchor window, wherever active binds moved it. Scroll overflow | |
| 304 | 304 | // keeps each column's own width. | |
| 305 | - | if w.policy().equalCells { | |
| 305 | + | if w.policy().maximizeHides() { | |
| 306 | 306 | w.unmaximize() | |
| 307 | 307 | for i := range w.Columns { | |
| 308 | 308 | if s.fullWidth != 0 && slices.Contains(w.Columns[i].Windows, s.fullWidth) { | |
| @@ -460,11 +460,8 @@ func (m *Monitor) overviewAcross(dx int) { | |||
| 460 | 460 | m.clearCard() | |
| 461 | 461 | m.ov.selected = 0 | |
| 462 | 462 | if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 { | |
| 463 | - | // The front column: a maximized one keeps its cards behind. | |
| 464 | - | i := w.Focus | |
| 465 | - | if !w.hasBandCards() { | |
| 466 | - | i, _ = w.laneBounds(w.laneOf(w.Focus)) | |
| 467 | - | } | |
| 463 | + | // The first column of the focused lane. | |
| 464 | + | i, _ := w.laneBounds(w.laneOf(w.Focus)) | |
| 468 | 465 | m.selectLaneColumn(w, i) | |
| 469 | 466 | } | |
| 470 | 467 | return | |
| @@ -979,37 +976,16 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||
| 979 | 976 | rects = w.colBuf | |
| 980 | 977 | span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile. | |
| 981 | 978 | } | |
| 982 | - | // off is the room, left of the band, for the cards a maximized | |
| 983 | - | // cascade column hides (the same room is kept on its right). | |
| 984 | - | off := 0 | |
| 985 | - | if w.hasBandCards() { | |
| 986 | - | off = 2 * w.bandCardStep() | |
| 987 | - | // The columns the maximized one hides are cards behind it, | |
| 988 | - | // farthest first so the nearest paint over them. | |
| 989 | - | first, last := w.laneBounds(w.laneOf(w.Focus)) | |
| 990 | - | for d := last - first; d > 0; d-- { | |
| 991 | - | if i := w.Focus - d; i >= first { | |
| 992 | - | tiles = w.appendBandCard(tiles, i) | |
| 993 | - | } | |
| 994 | - | if i := w.Focus + d; i <= last { | |
| 995 | - | tiles = w.appendBandCard(tiles, i) | |
| 996 | - | } | |
| 997 | - | } | |
| 998 | - | } | |
| 999 | 979 | for i, c := range w.Columns { | |
| 1000 | 980 | r := Rect{Y: g, H: h} | |
| 1001 | 981 | if w.policy().wraps { | |
| 1002 | - | if w.hiddenByMaximized(i) { | |
| 1003 | - | continue // A card, above. | |
| 1004 | - | } | |
| 1005 | 982 | // The band is the lane: it is encoded in Y, one screen apart. | |
| 1006 | - | // A maximized column fills its band as on screen; the cards | |
| 1007 | - | // it hides peek out on either side, past the band. | |
| 1008 | - | r.X = off + w.cellX(i) | |
| 983 | + | // A maximized column fills its band, alone, as on screen. | |
| 984 | + | r.X = w.cellX(i) | |
| 1009 | 985 | r.Y = g + w.band(i)*w.Usable.H | |
| 1010 | 986 | r.W = w.cellWidth(i) | |
| 1011 | 987 | if c.FullWidth { | |
| 1012 | - | r.X, r.W = off+g, max(w.Usable.W-2*g, 0) | |
| 988 | + | r.X, r.W = g, max(w.Usable.W-2*g, 0) | |
| 1013 | 989 | } | |
| 1014 | 990 | } else if w.Overflow == OverflowFixed { | |
| 1015 | 991 | r = rects[i] | |
| @@ -1042,51 +1018,11 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||
| 1042 | 1018 | } | |
| 1043 | 1019 | } | |
| 1044 | 1020 | if w.policy().wraps && len(w.Columns) > 0 { | |
| 1045 | - | span = w.Usable.W + 2*off | |
| 1021 | + | span = w.Usable.W | |
| 1046 | 1022 | } | |
| 1047 | 1023 | return tiles, span, sel | |
| 1048 | 1024 | } | |
| 1049 | 1025 | ||
| 1050 | - | // bandCardStep is the unscaled step between the cards of a cascade band | |
| 1051 | - | // whose maximized column hides the others: the stack peek step. | |
| 1052 | - | func (w *Workspace) bandCardStep() int { | |
| 1053 | - | return max(1, int(math.Round(float64(w.Usable.H)*overviewPeekStep))) | |
| 1054 | - | } | |
| 1055 | - | ||
| 1056 | - | // hasBandCards reports whether the focused column of a cascade is | |
| 1057 | - | // maximized and hides other columns of its band: the overview shows them | |
| 1058 | - | // as cards behind it, peeking out up to two card steps on each side. | |
| 1059 | - | func (w *Workspace) hasBandCards() bool { | |
| 1060 | - | if !w.policy().wraps || w.Focus < 0 || w.Focus >= len(w.Columns) || !w.Columns[w.Focus].FullWidth { | |
| 1061 | - | return false | |
| 1062 | - | } | |
| 1063 | - | first, last := w.laneBounds(w.laneOf(w.Focus)) | |
| 1064 | - | return first < last | |
| 1065 | - | } | |
| 1066 | - | ||
| 1067 | - | // appendBandCard appends hidden column i of a cascade band as a dimmed card | |
| 1068 | - | // at its cell size behind the maximized column, on its side: the nearest | |
| 1069 | - | // column one step out from the maximized one, the next ones two steps out, | |
| 1070 | - | // like the cards of a stack. The maximized column keeps its full size two | |
| 1071 | - | // steps right of the row's start (see previewTilesInto). | |
| 1072 | - | func (w *Workspace) appendBandCard(tiles []Placement, i int) []Placement { | |
| 1073 | - | g, s, c := w.gap(), w.bandCardStep(), w.Columns[i] | |
| 1074 | - | r := Rect{Y: g + w.band(i)*w.Usable.H, W: w.cellWidth(i), H: max(w.Usable.H-2*g, 0)} | |
| 1075 | - | k := min(max(i-w.Focus, w.Focus-i), 2) | |
| 1076 | - | if i < w.Focus { | |
| 1077 | - | r.X = g + (2-k)*s | |
| 1078 | - | } else { | |
| 1079 | - | r.X = w.Usable.W - g + (2+k)*s - r.W | |
| 1080 | - | } | |
| 1081 | - | w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g) | |
| 1082 | - | for j, t := range w.rowBuf { | |
| 1083 | - | p := Placement{ID: c.Windows[j], Rect: t} | |
| 1084 | - | p.peeking(1) | |
| 1085 | - | tiles = append(tiles, p) | |
| 1086 | - | } | |
| 1087 | - | return tiles | |
| 1088 | - | } | |
| 1089 | - | ||
| 1090 | 1026 | // previewRow scales the tiles of w into a row placed in slot, centred | |
| 1091 | 1027 | // in the slot's width, or scrolled so the focused column shows when the | |
| 1092 | 1028 | // row is wider, right of the stash pile. Dimmed rows (neighbor | |
| @@ -1365,14 +1301,9 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||
| 1365 | 1301 | d := sign(m.ov.scrollX) | |
| 1366 | 1302 | if cascade && finger { | |
| 1367 | 1303 | before = m.overviewSelection() | |
| 1368 | - | // One step per gesture: a card behind a maximized | |
| 1369 | - | // column first, like the arrows, else one workspace, | |
| 1370 | - | // even over a stash card. | |
| 1371 | - | if m.bandCardsAt(m.Current()) { | |
| 1372 | - | m.OverviewMove(d, 0) | |
| 1373 | - | } else { | |
| 1374 | - | m.overviewStep(d) | |
| 1375 | - | } | |
| 1304 | + | // One step per gesture: one workspace, even over a | |
| 1305 | + | // stash card. | |
| 1306 | + | m.overviewStep(d) | |
| 1376 | 1307 | m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true | |
| 1377 | 1308 | return m.overviewSelection() != before | |
| 1378 | 1309 | } | |
| @@ -1392,12 +1323,6 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||
| 1392 | 1323 | } | |
| 1393 | 1324 | } | |
| 1394 | 1325 | ||
| 1395 | - | // bandCardsAt reports whether the selection of w is a maximized cascade | |
| 1396 | - | // column hiding cards of its band: not a stash card nor a covering float. | |
| 1397 | - | func (m *Monitor) bandCardsAt(w *Workspace) bool { | |
| 1398 | - | return m.cardAt(w) < 0 && w.hasBandCards() && m.stackFront(w).kind == stackColumns | |
| 1399 | - | } | |
| 1400 | - | ||
| 1401 | 1326 | // overviewSelection is what an overview step can move: the workspace, its | |
| 1402 | 1327 | // stack front, stash card, focused column and selected window. | |
| 1403 | 1328 | type overviewSelection struct { | |
| @@ -302,7 +302,7 @@ func restoreRow(w *Workspace, s rowSnapshot) { | |||||
| 302 | // Fixed overflow maximizes at most one column: only the one holding | 302 | // Fixed overflow maximizes at most one column: only the one holding | ||
| 303 | // the anchor window, wherever active binds moved it. Scroll overflow | 303 | // the anchor window, wherever active binds moved it. Scroll overflow | ||
| 304 | // keeps each column's own width. | 304 | // keeps each column's own width. | ||
| 305 | - | if w.policy().equalCells { | 305 | + | if w.policy().maximizeHides() { |
| 306 | w.unmaximize() | 306 | w.unmaximize() | ||
| 307 | for i := range w.Columns { | 307 | for i := range w.Columns { | ||
| 308 | if s.fullWidth != 0 && slices.Contains(w.Columns[i].Windows, s.fullWidth) { | 308 | if s.fullWidth != 0 && slices.Contains(w.Columns[i].Windows, s.fullWidth) { | ||
| @@ -460,11 +460,8 @@ func (m *Monitor) overviewAcross(dx int) { | |||||
| 460 | m.clearCard() | 460 | m.clearCard() | ||
| 461 | m.ov.selected = 0 | 461 | m.ov.selected = 0 | ||
| 462 | if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 { | 462 | if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 { | ||
| 463 | - | // The front column: a maximized one keeps its cards behind. | 463 | + | // The first column of the focused lane. |
| 464 | - | i := w.Focus | 464 | + | i, _ := w.laneBounds(w.laneOf(w.Focus)) |
| 465 | - | if !w.hasBandCards() { | |||
| 466 | - | i, _ = w.laneBounds(w.laneOf(w.Focus)) | |||
| 467 | - | } | |||
| 468 | m.selectLaneColumn(w, i) | 465 | m.selectLaneColumn(w, i) | ||
| 469 | } | 466 | } | ||
| 470 | return | 467 | return | ||
| @@ -979,37 +976,16 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||||
| 979 | rects = w.colBuf | 976 | rects = w.colBuf | ||
| 980 | span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile. | 977 | span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile. | ||
| 981 | } | 978 | } | ||
| 982 | - | // off is the room, left of the band, for the cards a maximized | |||
| 983 | - | // cascade column hides (the same room is kept on its right). | |||
| 984 | - | off := 0 | |||
| 985 | - | if w.hasBandCards() { | |||
| 986 | - | off = 2 * w.bandCardStep() | |||
| 987 | - | // The columns the maximized one hides are cards behind it, | |||
| 988 | - | // farthest first so the nearest paint over them. | |||
| 989 | - | first, last := w.laneBounds(w.laneOf(w.Focus)) | |||
| 990 | - | for d := last - first; d > 0; d-- { | |||
| 991 | - | if i := w.Focus - d; i >= first { | |||
| 992 | - | tiles = w.appendBandCard(tiles, i) | |||
| 993 | - | } | |||
| 994 | - | if i := w.Focus + d; i <= last { | |||
| 995 | - | tiles = w.appendBandCard(tiles, i) | |||
| 996 | - | } | |||
| 997 | - | } | |||
| 998 | - | } | |||
| 999 | for i, c := range w.Columns { | 979 | for i, c := range w.Columns { | ||
| 1000 | r := Rect{Y: g, H: h} | 980 | r := Rect{Y: g, H: h} | ||
| 1001 | if w.policy().wraps { | 981 | if w.policy().wraps { | ||
| 1002 | - | if w.hiddenByMaximized(i) { | |||
| 1003 | - | continue // A card, above. | |||
| 1004 | - | } | |||
| 1005 | // The band is the lane: it is encoded in Y, one screen apart. | 982 | // The band is the lane: it is encoded in Y, one screen apart. | ||
| 1006 | - | // A maximized column fills its band as on screen; the cards | 983 | + | // A maximized column fills its band, alone, as on screen. |
| 1007 | - | // it hides peek out on either side, past the band. | 984 | + | r.X = w.cellX(i) |
| 1008 | - | r.X = off + w.cellX(i) | |||
| 1009 | r.Y = g + w.band(i)*w.Usable.H | 985 | r.Y = g + w.band(i)*w.Usable.H | ||
| 1010 | r.W = w.cellWidth(i) | 986 | r.W = w.cellWidth(i) | ||
| 1011 | if c.FullWidth { | 987 | if c.FullWidth { | ||
| 1012 | - | r.X, r.W = off+g, max(w.Usable.W-2*g, 0) | 988 | + | r.X, r.W = g, max(w.Usable.W-2*g, 0) |
| 1013 | } | 989 | } | ||
| 1014 | } else if w.Overflow == OverflowFixed { | 990 | } else if w.Overflow == OverflowFixed { | ||
| 1015 | r = rects[i] | 991 | r = rects[i] | ||
| @@ -1042,51 +1018,11 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||||
| 1042 | } | 1018 | } | ||
| 1043 | } | 1019 | } | ||
| 1044 | if w.policy().wraps && len(w.Columns) > 0 { | 1020 | if w.policy().wraps && len(w.Columns) > 0 { | ||
| 1045 | - | span = w.Usable.W + 2*off | 1021 | + | span = w.Usable.W |
| 1046 | } | 1022 | } | ||
| 1047 | return tiles, span, sel | 1023 | return tiles, span, sel | ||
| 1048 | } | 1024 | } | ||
| 1049 | 1025 | ||||
| 1050 | - | // bandCardStep is the unscaled step between the cards of a cascade band | |||
| 1051 | - | // whose maximized column hides the others: the stack peek step. | |||
| 1052 | - | func (w *Workspace) bandCardStep() int { | |||
| 1053 | - | return max(1, int(math.Round(float64(w.Usable.H)*overviewPeekStep))) | |||
| 1054 | - | } | |||
| 1055 | - | ||||
| 1056 | - | // hasBandCards reports whether the focused column of a cascade is | |||
| 1057 | - | // maximized and hides other columns of its band: the overview shows them | |||
| 1058 | - | // as cards behind it, peeking out up to two card steps on each side. | |||
| 1059 | - | func (w *Workspace) hasBandCards() bool { | |||
| 1060 | - | if !w.policy().wraps || w.Focus < 0 || w.Focus >= len(w.Columns) || !w.Columns[w.Focus].FullWidth { | |||
| 1061 | - | return false | |||
| 1062 | - | } | |||
| 1063 | - | first, last := w.laneBounds(w.laneOf(w.Focus)) | |||
| 1064 | - | return first < last | |||
| 1065 | - | } | |||
| 1066 | - | ||||
| 1067 | - | // appendBandCard appends hidden column i of a cascade band as a dimmed card | |||
| 1068 | - | // at its cell size behind the maximized column, on its side: the nearest | |||
| 1069 | - | // column one step out from the maximized one, the next ones two steps out, | |||
| 1070 | - | // like the cards of a stack. The maximized column keeps its full size two | |||
| 1071 | - | // steps right of the row's start (see previewTilesInto). | |||
| 1072 | - | func (w *Workspace) appendBandCard(tiles []Placement, i int) []Placement { | |||
| 1073 | - | g, s, c := w.gap(), w.bandCardStep(), w.Columns[i] | |||
| 1074 | - | r := Rect{Y: g + w.band(i)*w.Usable.H, W: w.cellWidth(i), H: max(w.Usable.H-2*g, 0)} | |||
| 1075 | - | k := min(max(i-w.Focus, w.Focus-i), 2) | |||
| 1076 | - | if i < w.Focus { | |||
| 1077 | - | r.X = g + (2-k)*s | |||
| 1078 | - | } else { | |||
| 1079 | - | r.X = w.Usable.W - g + (2+k)*s - r.W | |||
| 1080 | - | } | |||
| 1081 | - | w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g) | |||
| 1082 | - | for j, t := range w.rowBuf { | |||
| 1083 | - | p := Placement{ID: c.Windows[j], Rect: t} | |||
| 1084 | - | p.peeking(1) | |||
| 1085 | - | tiles = append(tiles, p) | |||
| 1086 | - | } | |||
| 1087 | - | return tiles | |||
| 1088 | - | } | |||
| 1089 | - | ||||
| 1090 | // previewRow scales the tiles of w into a row placed in slot, centred | 1026 | // previewRow scales the tiles of w into a row placed in slot, centred | ||
| 1091 | // in the slot's width, or scrolled so the focused column shows when the | 1027 | // in the slot's width, or scrolled so the focused column shows when the | ||
| 1092 | // row is wider, right of the stash pile. Dimmed rows (neighbor | 1028 | // row is wider, right of the stash pile. Dimmed rows (neighbor | ||
| @@ -1365,14 +1301,9 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||||
| 1365 | d := sign(m.ov.scrollX) | 1301 | d := sign(m.ov.scrollX) | ||
| 1366 | if cascade && finger { | 1302 | if cascade && finger { | ||
| 1367 | before = m.overviewSelection() | 1303 | before = m.overviewSelection() | ||
| 1368 | - | // One step per gesture: a card behind a maximized | 1304 | + | // One step per gesture: one workspace, even over a |
| 1369 | - | // column first, like the arrows, else one workspace, | 1305 | + | // stash card. |
| 1370 | - | // even over a stash card. | 1306 | + | m.overviewStep(d) |
| 1371 | - | if m.bandCardsAt(m.Current()) { | |||
| 1372 | - | m.OverviewMove(d, 0) | |||
| 1373 | - | } else { | |||
| 1374 | - | m.overviewStep(d) | |||
| 1375 | - | } | |||
| 1376 | m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true | 1307 | m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true | ||
| 1377 | return m.overviewSelection() != before | 1308 | return m.overviewSelection() != before | ||
| 1378 | } | 1309 | } | ||
| @@ -1392,12 +1323,6 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||||
| 1392 | } | 1323 | } | ||
| 1393 | } | 1324 | } | ||
| 1394 | 1325 | ||||
| 1395 | - | // bandCardsAt reports whether the selection of w is a maximized cascade | |||
| 1396 | - | // column hiding cards of its band: not a stash card nor a covering float. | |||
| 1397 | - | func (m *Monitor) bandCardsAt(w *Workspace) bool { | |||
| 1398 | - | return m.cardAt(w) < 0 && w.hasBandCards() && m.stackFront(w).kind == stackColumns | |||
| 1399 | - | } | |||
| 1400 | - | ||||
| 1401 | // overviewSelection is what an overview step can move: the workspace, its | 1326 | // overviewSelection is what an overview step can move: the workspace, its | ||
| 1402 | // stack front, stash card, focused column and selected window. | 1327 | // stack front, stash card, focused column and selected window. | ||
| 1403 | type overviewSelection struct { | 1328 | type overviewSelection struct { | ||
M internal/core/overview_cascade_test.go +0 -290
| @@ -75,182 +75,6 @@ func TestOverviewCascadeZoomIgnoresBandCount(t *testing.T) { | |||
| 75 | 75 | } | |
| 76 | 76 | } | |
| 77 | 77 | ||
| 78 | - | // A maximized column keeps its full size in the preview (the workspace's | |
| 79 | - | // ratio); the columns it hides are dimmed cards at their cell size behind | |
| 80 | - | // it, peeking out on their side, the nearest one step, the farther ones two. | |
| 81 | - | // The row widens by that room; other bands keep their cells. | |
| 82 | - | func TestOverviewCascadeMaximizedColumnCards(t *testing.T) { | |
| 83 | - | // Four columns, three per band: band 0 holds 0..2, band 1 holds 3. | |
| 84 | - | for _, tc := range []struct { | |
| 85 | - | n, max int | |
| 86 | - | cards []int // per column: 0 not a card, -k/+k k steps left/right | |
| 87 | - | order []int // paint order of band 0's columns | |
| 88 | - | }{ | |
| 89 | - | {n: 1, max: 0, cards: []int{0}, order: []int{0}}, | |
| 90 | - | {n: 4, max: 0, cards: []int{0, 1, 2, 0}, order: []int{2, 1, 0}}, | |
| 91 | - | {n: 4, max: 1, cards: []int{-1, 0, 1, 0}, order: []int{0, 2, 1}}, | |
| 92 | - | {n: 4, max: 3, cards: []int{0, 0, 0, 0}}, | |
| 93 | - | // Partial bands: the card has its band's half width. | |
| 94 | - | {n: 2, max: 0, cards: []int{0, 1}, order: []int{1, 0}}, | |
| 95 | - | {n: 5, max: 4, cards: []int{0, 0, 0, -1, 0}}, | |
| 96 | - | } { | |
| 97 | - | w := cascadeWorkspace(tc.n) | |
| 98 | - | w.Focus = tc.max | |
| 99 | - | w.maximize(tc.max) | |
| 100 | - | tiles, span, sel := w.previewTiles() | |
| 101 | - | g, h, s := w.gap(), w.Usable.H-2*w.gap(), w.bandCardStep() | |
| 102 | - | off := 0 | |
| 103 | - | if slices.ContainsFunc(tc.cards, func(k int) bool { return k != 0 }) { | |
| 104 | - | off = 2 * s | |
| 105 | - | } | |
| 106 | - | if span != w.Usable.W+2*off { | |
| 107 | - | t.Fatalf("n=%d max=%d span %d, want %d", tc.n, tc.max, span, w.Usable.W+2*off) | |
| 108 | - | } | |
| 109 | - | var order []int | |
| 110 | - | for _, p := range tiles { | |
| 111 | - | i := int(p.ID) - 1 | |
| 112 | - | if i/3 == 0 && tc.order != nil { | |
| 113 | - | order = append(order, i) | |
| 114 | - | } | |
| 115 | - | band := i / 3 | |
| 116 | - | want := Rect{X: off + w.cellX(i), Y: g + band*w.Usable.H, W: w.cellWidth(i), H: h} | |
| 117 | - | switch k := tc.cards[i]; { | |
| 118 | - | case i == tc.max: | |
| 119 | - | // Full size, as on screen: the workspace's ratio. | |
| 120 | - | want.X, want.W = off+g, w.Usable.W-2*g | |
| 121 | - | case k < 0: | |
| 122 | - | want.X = g + (2+k)*s | |
| 123 | - | case k > 0: | |
| 124 | - | want.X = w.Usable.W - g + (2+k)*s - want.W | |
| 125 | - | } | |
| 126 | - | if p.Rect.X < 0 || p.Rect.X+p.Rect.W > span { | |
| 127 | - | t.Fatalf("n=%d max=%d column %d %+v outside span %d", tc.n, tc.max, i, p.Rect, span) | |
| 128 | - | } | |
| 129 | - | if p.Hidden || p.Peek != (tc.cards[i] != 0) || p.Focused != (i == tc.max) || p.Rect != want { | |
| 130 | - | t.Fatalf("n=%d max=%d column %d: %+v, want rect %+v card %v", tc.n, tc.max, i, p, want, tc.cards[i] != 0) | |
| 131 | - | } | |
| 132 | - | // The 900-wide band split among its columns, no gaps. | |
| 133 | - | if inBand := min(tc.n-band*3, 3); i != tc.max && p.Rect.W != 900/inBand { | |
| 134 | - | t.Fatalf("n=%d max=%d column %d width %d, want %d", tc.n, tc.max, i, p.Rect.W, 900/inBand) | |
| 135 | - | } | |
| 136 | - | if i == tc.max && sel != want { | |
| 137 | - | t.Fatalf("n=%d max=%d sel %+v, want %+v", tc.n, tc.max, sel, want) | |
| 138 | - | } | |
| 139 | - | } | |
| 140 | - | if tc.order != nil && !slices.Equal(order, tc.order) { | |
| 141 | - | t.Fatalf("n=%d max=%d paint order %v, want %v", tc.n, tc.max, order, tc.order) | |
| 142 | - | } | |
| 143 | - | } | |
| 144 | - | } | |
| 145 | - | ||
| 146 | - | // Moving onto a card behind a maximized column hands it the maximization; | |
| 147 | - | // Return keeps it there, Escape gives it back. Moving to another band | |
| 148 | - | // unmaximizes as before. | |
| 149 | - | func TestOverviewCascadeMaximizedCardSelection(t *testing.T) { | |
| 150 | - | for _, d := range cascadeDrivers { | |
| 151 | - | t.Run(d.name, func(t *testing.T) { | |
| 152 | - | m := cascadeOverview() | |
| 153 | - | w := m.Current() | |
| 154 | - | w.FocusID(9) // band 2 holds 7, 8, 9 | |
| 155 | - | w.ToggleFullWidth() | |
| 156 | - | m.ToggleOverview() | |
| 157 | - | for _, id := range []WindowID{7, 8} { | |
| 158 | - | if p := previewOf(t, m.Layout(), id); p.Hidden || !p.Peek { | |
| 159 | - | t.Fatalf("card %d: %+v", id, p) | |
| 160 | - | } | |
| 161 | - | } | |
| 162 | - | d.move(m, -1, 0) | |
| 163 | - | if focusedID(m) != 8 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(9)].FullWidth { | |
| 164 | - | t.Fatalf("left: focus %d, maximization not moved", focusedID(m)) | |
| 165 | - | } | |
| 166 | - | if p := previewOf(t, m.Layout(), 9); !p.Peek { | |
| 167 | - | t.Fatalf("former maximized column not a card: %+v", p) | |
| 168 | - | } | |
| 169 | - | m.CancelOverview() | |
| 170 | - | if focusedID(m) != 9 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(8)].FullWidth { | |
| 171 | - | t.Fatalf("escape: focus %d, maximization not restored", focusedID(m)) | |
| 172 | - | } | |
| 173 | - | ||
| 174 | - | m.ToggleOverview() | |
| 175 | - | d.move(m, -1, 0) | |
| 176 | - | d.move(m, -1, 0) | |
| 177 | - | m.ToggleOverview() // return | |
| 178 | - | if focusedID(m) != 7 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(9)].FullWidth { | |
| 179 | - | t.Fatalf("return: focus %d, maximization not on 7", focusedID(m)) | |
| 180 | - | } | |
| 181 | - | ||
| 182 | - | m.ToggleOverview() | |
| 183 | - | d.move(m, 0, -1) | |
| 184 | - | if focusedID(m) != 4 || slices.ContainsFunc(w.Columns, func(c Column) bool { return c.FullWidth }) { | |
| 185 | - | t.Fatalf("up: focus %d, a column still maximized", focusedID(m)) | |
| 186 | - | } | |
| 187 | - | }) | |
| 188 | - | } | |
| 189 | - | } | |
| 190 | - | ||
| 191 | - | // A click on a window of a card's column records that window in the | |
| 192 | - | // maximize history, not the column's former focus. | |
| 193 | - | func TestOverviewCascadeMaximizedCardPickRecordsWindow(t *testing.T) { | |
| 194 | - | m := cascadeOverview() | |
| 195 | - | w := m.Current() | |
| 196 | - | // Column 8 stacks 8 over 50, 8 focused. | |
| 197 | - | w.Columns[w.columnOf(8)].Windows = append(w.Columns[w.columnOf(8)].Windows, 50) | |
| 198 | - | w.FocusID(7) | |
| 199 | - | w.ToggleFullWidth() | |
| 200 | - | m.ToggleOverview() | |
| 201 | - | m.OverviewPick(50) | |
| 202 | - | if len(w.maximized) == 0 || w.maximized[0] != 50 || focusedID(m) != 50 { | |
| 203 | - | t.Fatalf("history %v, focus %d; want 50 first", w.maximized, focusedID(m)) | |
| 204 | - | } | |
| 205 | - | } | |
| 206 | - | ||
| 207 | - | // A FullWidth column outside the focused band fills its band, shifted like | |
| 208 | - | // every tile by the room of the focused column's cards. | |
| 209 | - | func TestOverviewCascadeUnfocusedMaximizedFillsBand(t *testing.T) { | |
| 210 | - | w := cascadeWorkspace(6) | |
| 211 | - | w.SetGaps(8) | |
| 212 | - | w.Focus = 4 | |
| 213 | - | w.maximize(4) | |
| 214 | - | w.Columns[0].FullWidth = true | |
| 215 | - | tiles, _, _ := w.previewTiles() | |
| 216 | - | // The row keeps room for the focused column's cards on its left. | |
| 217 | - | g, off := w.gap(), 2*w.bandCardStep() | |
| 218 | - | p := previewOf(t, tiles, 1) | |
| 219 | - | if p.Hidden || p.Rect.X != off+g || p.Rect.W != w.Usable.W-2*g { | |
| 220 | - | t.Fatalf("unfocused maximized column %+v, want X %d W %d", p, off+g, w.Usable.W-2*g) | |
| 221 | - | } | |
| 222 | - | } | |
| 223 | - | ||
| 224 | - | // A click on a card behind a maximized cascade column hands it the | |
| 225 | - | // maximization. | |
| 226 | - | func TestOverviewCascadeMaximizedCardPick(t *testing.T) { | |
| 227 | - | m := cascadeOverview() | |
| 228 | - | w := m.Current() | |
| 229 | - | w.FocusID(7) | |
| 230 | - | w.ToggleFullWidth() | |
| 231 | - | m.ToggleOverview() | |
| 232 | - | // Each card's strip right of the maximized column hits it: 8 next to | |
| 233 | - | // it, 9 at the band's edge. | |
| 234 | - | ps := m.Layout() | |
| 235 | - | front, near, far := previewOf(t, ps, 7).Rect, previewOf(t, ps, 8).Rect, previewOf(t, ps, 9).Rect | |
| 236 | - | for _, c := range []struct { | |
| 237 | - | x0, x1 int | |
| 238 | - | want WindowID | |
| 239 | - | }{{front.X + front.W, near.X + near.W, 8}, {near.X + near.W, far.X + far.W, 9}} { | |
| 240 | - | x := (c.x0 + c.x1) / 2 | |
| 241 | - | if c.x0 >= c.x1 { | |
| 242 | - | t.Fatalf("no strip for %d: front %v near %v far %v", c.want, front, near, far) | |
| 243 | - | } | |
| 244 | - | if got := overviewIn(ps, float64(x), float64(centreY(far))); got != c.want { | |
| 245 | - | t.Fatalf("strip at x=%d hits %d, want %d", x, got, c.want) | |
| 246 | - | } | |
| 247 | - | } | |
| 248 | - | m.OverviewPick(9) | |
| 249 | - | if m.ov.open || focusedID(m) != 9 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(7)].FullWidth { | |
| 250 | - | t.Fatalf("pick: open %v, focus %d, maximization not moved", m.ov.open, focusedID(m)) | |
| 251 | - | } | |
| 252 | - | } | |
| 253 | - | ||
| 254 | 78 | // The selected band is the centre lane: undimmed, centred, selected tile | |
| 255 | 79 | // focused. | |
| 256 | 80 | func TestOverviewCascadeCentreLane(t *testing.T) { | |
| @@ -643,120 +467,6 @@ func TestOverviewCascadeThreeFinger(t *testing.T) { | |||
| 643 | 467 | } | |
| 644 | 468 | } | |
| 645 | 469 | ||
| 646 | - | // Three fingers sideways walk the cards behind a maximized column one per | |
| 647 | - | // gesture, like the arrows, then change workspace past the band's edge. | |
| 648 | - | func TestOverviewCascadeThreeFingerMaximizedCards(t *testing.T) { | |
| 649 | - | c, m := cascadeCore(t) | |
| 650 | - | w := m.Current() | |
| 651 | - | w.ToggleFullWidth() // Column 2 hides 3, the other column of its band. | |
| 652 | - | m.ToggleOverview() | |
| 653 | - | swipe := func(dx float64) { | |
| 654 | - | t.Helper() | |
| 655 | - | c.swipeBegin(ports.SwipeBegin{Fingers: 3}) | |
| 656 | - | for range 4 { | |
| 657 | - | c.swipeUpdate(ports.SwipeUpdate{DX: dx}) | |
| 658 | - | } | |
| 659 | - | c.swipeEnd(ports.SwipeEnd{}) | |
| 660 | - | } | |
| 661 | - | swipe(40) | |
| 662 | - | if m.Current() != w || m.overviewTarget() != 3 || !w.Columns[w.Focus].FullWidth { | |
| 663 | - | t.Fatalf("swipe right: workspace %d, target %d, want maximized 3", indexOf(m.Workspaces, m.Current()), m.overviewTarget()) | |
| 664 | - | } | |
| 665 | - | swipe(-40) | |
| 666 | - | if m.Current() != w || m.overviewTarget() != 2 || !w.Columns[w.Focus].FullWidth { | |
| 667 | - | t.Fatalf("swipe left: workspace %d, target %d, want maximized 2", indexOf(m.Workspaces, m.Current()), m.overviewTarget()) | |
| 668 | - | } | |
| 669 | - | swipe(40) | |
| 670 | - | swipe(40) | |
| 671 | - | if m.Current() != m.Workspaces[2] { | |
| 672 | - | t.Fatalf("swipe right past the band: workspace %d", indexOf(m.Workspaces, m.Current())) | |
| 673 | - | } | |
| 674 | - | ||
| 675 | - | // A covering float in front: the swipe changes workspace, as without | |
| 676 | - | // a maximized column. | |
| 677 | - | m.CancelOverview() | |
| 678 | - | w.AddFloating(90, 300, 200) | |
| 679 | - | m.ToggleOverview() | |
| 680 | - | if f := m.stackFront(w); f.kind != stackFloat || !w.Columns[w.Focus].FullWidth { | |
| 681 | - | t.Fatalf("setup: front %+v", f) | |
| 682 | - | } | |
| 683 | - | swipe(-40) | |
| 684 | - | if m.Current() != m.Workspaces[0] { | |
| 685 | - | t.Fatalf("swipe left over a float: workspace %d", indexOf(m.Workspaces, m.Current())) | |
| 686 | - | } | |
| 687 | - | } | |
| 688 | - | ||
| 689 | - | // Left from the first column of a band whose maximized column hides it: | |
| 690 | - | // the arrows and three fingers walk the cards, then enter the stash, or | |
| 691 | - | // change workspace without one. | |
| 692 | - | func TestOverviewCascadeMaximizedLeftEdge(t *testing.T) { | |
| 693 | - | for _, stash := range []bool{false, true} { | |
| 694 | - | c, m := cascadeCore(t) | |
| 695 | - | w := m.Current() | |
| 696 | - | if stash { | |
| 697 | - | w.AddWindow(40) | |
| 698 | - | w.FocusID(40) | |
| 699 | - | w.ToggleWindowStash() | |
| 700 | - | } | |
| 701 | - | w.FocusID(3) // Band {2, 3}: 3 hides 2. | |
| 702 | - | w.ToggleFullWidth() | |
| 703 | - | m.ToggleOverview() | |
| 704 | - | swipe := func() { | |
| 705 | - | c.swipeBegin(ports.SwipeBegin{Fingers: 3}) | |
| 706 | - | for range 4 { | |
| 707 | - | c.swipeUpdate(ports.SwipeUpdate{DX: -40}) | |
| 708 | - | } | |
| 709 | - | c.swipeEnd(ports.SwipeEnd{}) | |
| 710 | - | } | |
| 711 | - | swipe() | |
| 712 | - | if m.Current() != w || m.overviewTarget() != 2 || !w.Columns[w.Focus].FullWidth { | |
| 713 | - | t.Fatalf("stash %v: first swipe: target %d", stash, m.overviewTarget()) | |
| 714 | - | } | |
| 715 | - | swipe() | |
| 716 | - | if stash { | |
| 717 | - | if m.Current() != w || m.cardAt(w) < 0 { | |
| 718 | - | t.Fatalf("past the band: workspace %d, card %d; want the stash", indexOf(m.Workspaces, m.Current()), m.cardAt(w)) | |
| 719 | - | } | |
| 720 | - | } else if m.Current() != m.Workspaces[0] { | |
| 721 | - | t.Fatalf("past the band: workspace %d, want 0", indexOf(m.Workspaces, m.Current())) | |
| 722 | - | } | |
| 723 | - | } | |
| 724 | - | } | |
| 725 | - | ||
| 726 | - | // With four or more columns in a band, the cards three or more columns | |
| 727 | - | // away sit under the two-step card: arrows still reach every one, in | |
| 728 | - | // order, each taking the maximization. | |
| 729 | - | func TestOverviewCascadeFarCardsByArrows(t *testing.T) { | |
| 730 | - | m := newMonitor("", "") | |
| 731 | - | m.SetOutput(1200, 600) | |
| 732 | - | m.SetMaxColumns(5) | |
| 733 | - | w := m.Current() | |
| 734 | - | w.Overflow = OverflowCascade | |
| 735 | - | for id := WindowID(1); id <= 5; id++ { | |
| 736 | - | m.AddWindow(id) | |
| 737 | - | } | |
| 738 | - | w.FocusID(1) | |
| 739 | - | w.ToggleFullWidth() | |
| 740 | - | m.ToggleOverview() | |
| 741 | - | ps := m.Layout() | |
| 742 | - | edge := previewOf(t, ps, 3).Rect | |
| 743 | - | for _, id := range []WindowID{4, 5} { | |
| 744 | - | if p := previewOf(t, ps, id); p.Hidden || !p.Peek || p.Rect.X != edge.X { | |
| 745 | - | t.Fatalf("far card %d: %+v, want a card under 3 at x %d", id, p, edge.X) | |
| 746 | - | } | |
| 747 | - | } | |
| 748 | - | for _, want := range []WindowID{2, 3, 4, 5} { | |
| 749 | - | m.OverviewMove(1, 0) | |
| 750 | - | if m.Current() != w || m.overviewTarget() != want || !w.Columns[w.Focus].FullWidth { | |
| 751 | - | t.Fatalf("right: target %d, want maximized %d", m.overviewTarget(), want) | |
| 752 | - | } | |
| 753 | - | } | |
| 754 | - | m.CancelOverview() | |
| 755 | - | if focusedID(m) != 1 || !w.Columns[w.Focus].FullWidth { | |
| 756 | - | t.Fatalf("escape: focus %d, want maximized 1", focusedID(m)) | |
| 757 | - | } | |
| 758 | - | } | |
| 759 | - | ||
| 760 | 470 | // Right from a stash card lands on the maximized column, which keeps its | |
| 761 | 471 | // maximization, not on the first card of its band. | |
| 762 | 472 | func TestOverviewCascadeStashToMaximized(t *testing.T) { | |
| @@ -75,182 +75,6 @@ func TestOverviewCascadeZoomIgnoresBandCount(t *testing.T) { | |||||
| 75 | } | 75 | } | ||
| 76 | } | 76 | } | ||
| 77 | 77 | ||||
| 78 | - | // A maximized column keeps its full size in the preview (the workspace's | |||
| 79 | - | // ratio); the columns it hides are dimmed cards at their cell size behind | |||
| 80 | - | // it, peeking out on their side, the nearest one step, the farther ones two. | |||
| 81 | - | // The row widens by that room; other bands keep their cells. | |||
| 82 | - | func TestOverviewCascadeMaximizedColumnCards(t *testing.T) { | |||
| 83 | - | // Four columns, three per band: band 0 holds 0..2, band 1 holds 3. | |||
| 84 | - | for _, tc := range []struct { | |||
| 85 | - | n, max int | |||
| 86 | - | cards []int // per column: 0 not a card, -k/+k k steps left/right | |||
| 87 | - | order []int // paint order of band 0's columns | |||
| 88 | - | }{ | |||
| 89 | - | {n: 1, max: 0, cards: []int{0}, order: []int{0}}, | |||
| 90 | - | {n: 4, max: 0, cards: []int{0, 1, 2, 0}, order: []int{2, 1, 0}}, | |||
| 91 | - | {n: 4, max: 1, cards: []int{-1, 0, 1, 0}, order: []int{0, 2, 1}}, | |||
| 92 | - | {n: 4, max: 3, cards: []int{0, 0, 0, 0}}, | |||
| 93 | - | // Partial bands: the card has its band's half width. | |||
| 94 | - | {n: 2, max: 0, cards: []int{0, 1}, order: []int{1, 0}}, | |||
| 95 | - | {n: 5, max: 4, cards: []int{0, 0, 0, -1, 0}}, | |||
| 96 | - | } { | |||
| 97 | - | w := cascadeWorkspace(tc.n) | |||
| 98 | - | w.Focus = tc.max | |||
| 99 | - | w.maximize(tc.max) | |||
| 100 | - | tiles, span, sel := w.previewTiles() | |||
| 101 | - | g, h, s := w.gap(), w.Usable.H-2*w.gap(), w.bandCardStep() | |||
| 102 | - | off := 0 | |||
| 103 | - | if slices.ContainsFunc(tc.cards, func(k int) bool { return k != 0 }) { | |||
| 104 | - | off = 2 * s | |||
| 105 | - | } | |||
| 106 | - | if span != w.Usable.W+2*off { | |||
| 107 | - | t.Fatalf("n=%d max=%d span %d, want %d", tc.n, tc.max, span, w.Usable.W+2*off) | |||
| 108 | - | } | |||
| 109 | - | var order []int | |||
| 110 | - | for _, p := range tiles { | |||
| 111 | - | i := int(p.ID) - 1 | |||
| 112 | - | if i/3 == 0 && tc.order != nil { | |||
| 113 | - | order = append(order, i) | |||
| 114 | - | } | |||
| 115 | - | band := i / 3 | |||
| 116 | - | want := Rect{X: off + w.cellX(i), Y: g + band*w.Usable.H, W: w.cellWidth(i), H: h} | |||
| 117 | - | switch k := tc.cards[i]; { | |||
| 118 | - | case i == tc.max: | |||
| 119 | - | // Full size, as on screen: the workspace's ratio. | |||
| 120 | - | want.X, want.W = off+g, w.Usable.W-2*g | |||
| 121 | - | case k < 0: | |||
| 122 | - | want.X = g + (2+k)*s | |||
| 123 | - | case k > 0: | |||
| 124 | - | want.X = w.Usable.W - g + (2+k)*s - want.W | |||
| 125 | - | } | |||
| 126 | - | if p.Rect.X < 0 || p.Rect.X+p.Rect.W > span { | |||
| 127 | - | t.Fatalf("n=%d max=%d column %d %+v outside span %d", tc.n, tc.max, i, p.Rect, span) | |||
| 128 | - | } | |||
| 129 | - | if p.Hidden || p.Peek != (tc.cards[i] != 0) || p.Focused != (i == tc.max) || p.Rect != want { | |||
| 130 | - | t.Fatalf("n=%d max=%d column %d: %+v, want rect %+v card %v", tc.n, tc.max, i, p, want, tc.cards[i] != 0) | |||
| 131 | - | } | |||
| 132 | - | // The 900-wide band split among its columns, no gaps. | |||
| 133 | - | if inBand := min(tc.n-band*3, 3); i != tc.max && p.Rect.W != 900/inBand { | |||
| 134 | - | t.Fatalf("n=%d max=%d column %d width %d, want %d", tc.n, tc.max, i, p.Rect.W, 900/inBand) | |||
| 135 | - | } | |||
| 136 | - | if i == tc.max && sel != want { | |||
| 137 | - | t.Fatalf("n=%d max=%d sel %+v, want %+v", tc.n, tc.max, sel, want) | |||
| 138 | - | } | |||
| 139 | - | } | |||
| 140 | - | if tc.order != nil && !slices.Equal(order, tc.order) { | |||
| 141 | - | t.Fatalf("n=%d max=%d paint order %v, want %v", tc.n, tc.max, order, tc.order) | |||
| 142 | - | } | |||
| 143 | - | } | |||
| 144 | - | } | |||
| 145 | - | ||||
| 146 | - | // Moving onto a card behind a maximized column hands it the maximization; | |||
| 147 | - | // Return keeps it there, Escape gives it back. Moving to another band | |||
| 148 | - | // unmaximizes as before. | |||
| 149 | - | func TestOverviewCascadeMaximizedCardSelection(t *testing.T) { | |||
| 150 | - | for _, d := range cascadeDrivers { | |||
| 151 | - | t.Run(d.name, func(t *testing.T) { | |||
| 152 | - | m := cascadeOverview() | |||
| 153 | - | w := m.Current() | |||
| 154 | - | w.FocusID(9) // band 2 holds 7, 8, 9 | |||
| 155 | - | w.ToggleFullWidth() | |||
| 156 | - | m.ToggleOverview() | |||
| 157 | - | for _, id := range []WindowID{7, 8} { | |||
| 158 | - | if p := previewOf(t, m.Layout(), id); p.Hidden || !p.Peek { | |||
| 159 | - | t.Fatalf("card %d: %+v", id, p) | |||
| 160 | - | } | |||
| 161 | - | } | |||
| 162 | - | d.move(m, -1, 0) | |||
| 163 | - | if focusedID(m) != 8 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(9)].FullWidth { | |||
| 164 | - | t.Fatalf("left: focus %d, maximization not moved", focusedID(m)) | |||
| 165 | - | } | |||
| 166 | - | if p := previewOf(t, m.Layout(), 9); !p.Peek { | |||
| 167 | - | t.Fatalf("former maximized column not a card: %+v", p) | |||
| 168 | - | } | |||
| 169 | - | m.CancelOverview() | |||
| 170 | - | if focusedID(m) != 9 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(8)].FullWidth { | |||
| 171 | - | t.Fatalf("escape: focus %d, maximization not restored", focusedID(m)) | |||
| 172 | - | } | |||
| 173 | - | ||||
| 174 | - | m.ToggleOverview() | |||
| 175 | - | d.move(m, -1, 0) | |||
| 176 | - | d.move(m, -1, 0) | |||
| 177 | - | m.ToggleOverview() // return | |||
| 178 | - | if focusedID(m) != 7 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(9)].FullWidth { | |||
| 179 | - | t.Fatalf("return: focus %d, maximization not on 7", focusedID(m)) | |||
| 180 | - | } | |||
| 181 | - | ||||
| 182 | - | m.ToggleOverview() | |||
| 183 | - | d.move(m, 0, -1) | |||
| 184 | - | if focusedID(m) != 4 || slices.ContainsFunc(w.Columns, func(c Column) bool { return c.FullWidth }) { | |||
| 185 | - | t.Fatalf("up: focus %d, a column still maximized", focusedID(m)) | |||
| 186 | - | } | |||
| 187 | - | }) | |||
| 188 | - | } | |||
| 189 | - | } | |||
| 190 | - | ||||
| 191 | - | // A click on a window of a card's column records that window in the | |||
| 192 | - | // maximize history, not the column's former focus. | |||
| 193 | - | func TestOverviewCascadeMaximizedCardPickRecordsWindow(t *testing.T) { | |||
| 194 | - | m := cascadeOverview() | |||
| 195 | - | w := m.Current() | |||
| 196 | - | // Column 8 stacks 8 over 50, 8 focused. | |||
| 197 | - | w.Columns[w.columnOf(8)].Windows = append(w.Columns[w.columnOf(8)].Windows, 50) | |||
| 198 | - | w.FocusID(7) | |||
| 199 | - | w.ToggleFullWidth() | |||
| 200 | - | m.ToggleOverview() | |||
| 201 | - | m.OverviewPick(50) | |||
| 202 | - | if len(w.maximized) == 0 || w.maximized[0] != 50 || focusedID(m) != 50 { | |||
| 203 | - | t.Fatalf("history %v, focus %d; want 50 first", w.maximized, focusedID(m)) | |||
| 204 | - | } | |||
| 205 | - | } | |||
| 206 | - | ||||
| 207 | - | // A FullWidth column outside the focused band fills its band, shifted like | |||
| 208 | - | // every tile by the room of the focused column's cards. | |||
| 209 | - | func TestOverviewCascadeUnfocusedMaximizedFillsBand(t *testing.T) { | |||
| 210 | - | w := cascadeWorkspace(6) | |||
| 211 | - | w.SetGaps(8) | |||
| 212 | - | w.Focus = 4 | |||
| 213 | - | w.maximize(4) | |||
| 214 | - | w.Columns[0].FullWidth = true | |||
| 215 | - | tiles, _, _ := w.previewTiles() | |||
| 216 | - | // The row keeps room for the focused column's cards on its left. | |||
| 217 | - | g, off := w.gap(), 2*w.bandCardStep() | |||
| 218 | - | p := previewOf(t, tiles, 1) | |||
| 219 | - | if p.Hidden || p.Rect.X != off+g || p.Rect.W != w.Usable.W-2*g { | |||
| 220 | - | t.Fatalf("unfocused maximized column %+v, want X %d W %d", p, off+g, w.Usable.W-2*g) | |||
| 221 | - | } | |||
| 222 | - | } | |||
| 223 | - | ||||
| 224 | - | // A click on a card behind a maximized cascade column hands it the | |||
| 225 | - | // maximization. | |||
| 226 | - | func TestOverviewCascadeMaximizedCardPick(t *testing.T) { | |||
| 227 | - | m := cascadeOverview() | |||
| 228 | - | w := m.Current() | |||
| 229 | - | w.FocusID(7) | |||
| 230 | - | w.ToggleFullWidth() | |||
| 231 | - | m.ToggleOverview() | |||
| 232 | - | // Each card's strip right of the maximized column hits it: 8 next to | |||
| 233 | - | // it, 9 at the band's edge. | |||
| 234 | - | ps := m.Layout() | |||
| 235 | - | front, near, far := previewOf(t, ps, 7).Rect, previewOf(t, ps, 8).Rect, previewOf(t, ps, 9).Rect | |||
| 236 | - | for _, c := range []struct { | |||
| 237 | - | x0, x1 int | |||
| 238 | - | want WindowID | |||
| 239 | - | }{{front.X + front.W, near.X + near.W, 8}, {near.X + near.W, far.X + far.W, 9}} { | |||
| 240 | - | x := (c.x0 + c.x1) / 2 | |||
| 241 | - | if c.x0 >= c.x1 { | |||
| 242 | - | t.Fatalf("no strip for %d: front %v near %v far %v", c.want, front, near, far) | |||
| 243 | - | } | |||
| 244 | - | if got := overviewIn(ps, float64(x), float64(centreY(far))); got != c.want { | |||
| 245 | - | t.Fatalf("strip at x=%d hits %d, want %d", x, got, c.want) | |||
| 246 | - | } | |||
| 247 | - | } | |||
| 248 | - | m.OverviewPick(9) | |||
| 249 | - | if m.ov.open || focusedID(m) != 9 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(7)].FullWidth { | |||
| 250 | - | t.Fatalf("pick: open %v, focus %d, maximization not moved", m.ov.open, focusedID(m)) | |||
| 251 | - | } | |||
| 252 | - | } | |||
| 253 | - | ||||
| 254 | // The selected band is the centre lane: undimmed, centred, selected tile | 78 | // The selected band is the centre lane: undimmed, centred, selected tile | ||
| 255 | // focused. | 79 | // focused. | ||
| 256 | func TestOverviewCascadeCentreLane(t *testing.T) { | 80 | func TestOverviewCascadeCentreLane(t *testing.T) { | ||
| @@ -643,120 +467,6 @@ func TestOverviewCascadeThreeFinger(t *testing.T) { | |||||
| 643 | } | 467 | } | ||
| 644 | } | 468 | } | ||
| 645 | 469 | ||||
| 646 | - | // Three fingers sideways walk the cards behind a maximized column one per | |||
| 647 | - | // gesture, like the arrows, then change workspace past the band's edge. | |||
| 648 | - | func TestOverviewCascadeThreeFingerMaximizedCards(t *testing.T) { | |||
| 649 | - | c, m := cascadeCore(t) | |||
| 650 | - | w := m.Current() | |||
| 651 | - | w.ToggleFullWidth() // Column 2 hides 3, the other column of its band. | |||
| 652 | - | m.ToggleOverview() | |||
| 653 | - | swipe := func(dx float64) { | |||
| 654 | - | t.Helper() | |||
| 655 | - | c.swipeBegin(ports.SwipeBegin{Fingers: 3}) | |||
| 656 | - | for range 4 { | |||
| 657 | - | c.swipeUpdate(ports.SwipeUpdate{DX: dx}) | |||
| 658 | - | } | |||
| 659 | - | c.swipeEnd(ports.SwipeEnd{}) | |||
| 660 | - | } | |||
| 661 | - | swipe(40) | |||
| 662 | - | if m.Current() != w || m.overviewTarget() != 3 || !w.Columns[w.Focus].FullWidth { | |||
| 663 | - | t.Fatalf("swipe right: workspace %d, target %d, want maximized 3", indexOf(m.Workspaces, m.Current()), m.overviewTarget()) | |||
| 664 | - | } | |||
| 665 | - | swipe(-40) | |||
| 666 | - | if m.Current() != w || m.overviewTarget() != 2 || !w.Columns[w.Focus].FullWidth { | |||
| 667 | - | t.Fatalf("swipe left: workspace %d, target %d, want maximized 2", indexOf(m.Workspaces, m.Current()), m.overviewTarget()) | |||
| 668 | - | } | |||
| 669 | - | swipe(40) | |||
| 670 | - | swipe(40) | |||
| 671 | - | if m.Current() != m.Workspaces[2] { | |||
| 672 | - | t.Fatalf("swipe right past the band: workspace %d", indexOf(m.Workspaces, m.Current())) | |||
| 673 | - | } | |||
| 674 | - | ||||
| 675 | - | // A covering float in front: the swipe changes workspace, as without | |||
| 676 | - | // a maximized column. | |||
| 677 | - | m.CancelOverview() | |||
| 678 | - | w.AddFloating(90, 300, 200) | |||
| 679 | - | m.ToggleOverview() | |||
| 680 | - | if f := m.stackFront(w); f.kind != stackFloat || !w.Columns[w.Focus].FullWidth { | |||
| 681 | - | t.Fatalf("setup: front %+v", f) | |||
| 682 | - | } | |||
| 683 | - | swipe(-40) | |||
| 684 | - | if m.Current() != m.Workspaces[0] { | |||
| 685 | - | t.Fatalf("swipe left over a float: workspace %d", indexOf(m.Workspaces, m.Current())) | |||
| 686 | - | } | |||
| 687 | - | } | |||
| 688 | - | ||||
| 689 | - | // Left from the first column of a band whose maximized column hides it: | |||
| 690 | - | // the arrows and three fingers walk the cards, then enter the stash, or | |||
| 691 | - | // change workspace without one. | |||
| 692 | - | func TestOverviewCascadeMaximizedLeftEdge(t *testing.T) { | |||
| 693 | - | for _, stash := range []bool{false, true} { | |||
| 694 | - | c, m := cascadeCore(t) | |||
| 695 | - | w := m.Current() | |||
| 696 | - | if stash { | |||
| 697 | - | w.AddWindow(40) | |||
| 698 | - | w.FocusID(40) | |||
| 699 | - | w.ToggleWindowStash() | |||
| 700 | - | } | |||
| 701 | - | w.FocusID(3) // Band {2, 3}: 3 hides 2. | |||
| 702 | - | w.ToggleFullWidth() | |||
| 703 | - | m.ToggleOverview() | |||
| 704 | - | swipe := func() { | |||
| 705 | - | c.swipeBegin(ports.SwipeBegin{Fingers: 3}) | |||
| 706 | - | for range 4 { | |||
| 707 | - | c.swipeUpdate(ports.SwipeUpdate{DX: -40}) | |||
| 708 | - | } | |||
| 709 | - | c.swipeEnd(ports.SwipeEnd{}) | |||
| 710 | - | } | |||
| 711 | - | swipe() | |||
| 712 | - | if m.Current() != w || m.overviewTarget() != 2 || !w.Columns[w.Focus].FullWidth { | |||
| 713 | - | t.Fatalf("stash %v: first swipe: target %d", stash, m.overviewTarget()) | |||
| 714 | - | } | |||
| 715 | - | swipe() | |||
| 716 | - | if stash { | |||
| 717 | - | if m.Current() != w || m.cardAt(w) < 0 { | |||
| 718 | - | t.Fatalf("past the band: workspace %d, card %d; want the stash", indexOf(m.Workspaces, m.Current()), m.cardAt(w)) | |||
| 719 | - | } | |||
| 720 | - | } else if m.Current() != m.Workspaces[0] { | |||
| 721 | - | t.Fatalf("past the band: workspace %d, want 0", indexOf(m.Workspaces, m.Current())) | |||
| 722 | - | } | |||
| 723 | - | } | |||
| 724 | - | } | |||
| 725 | - | ||||
| 726 | - | // With four or more columns in a band, the cards three or more columns | |||
| 727 | - | // away sit under the two-step card: arrows still reach every one, in | |||
| 728 | - | // order, each taking the maximization. | |||
| 729 | - | func TestOverviewCascadeFarCardsByArrows(t *testing.T) { | |||
| 730 | - | m := newMonitor("", "") | |||
| 731 | - | m.SetOutput(1200, 600) | |||
| 732 | - | m.SetMaxColumns(5) | |||
| 733 | - | w := m.Current() | |||
| 734 | - | w.Overflow = OverflowCascade | |||
| 735 | - | for id := WindowID(1); id <= 5; id++ { | |||
| 736 | - | m.AddWindow(id) | |||
| 737 | - | } | |||
| 738 | - | w.FocusID(1) | |||
| 739 | - | w.ToggleFullWidth() | |||
| 740 | - | m.ToggleOverview() | |||
| 741 | - | ps := m.Layout() | |||
| 742 | - | edge := previewOf(t, ps, 3).Rect | |||
| 743 | - | for _, id := range []WindowID{4, 5} { | |||
| 744 | - | if p := previewOf(t, ps, id); p.Hidden || !p.Peek || p.Rect.X != edge.X { | |||
| 745 | - | t.Fatalf("far card %d: %+v, want a card under 3 at x %d", id, p, edge.X) | |||
| 746 | - | } | |||
| 747 | - | } | |||
| 748 | - | for _, want := range []WindowID{2, 3, 4, 5} { | |||
| 749 | - | m.OverviewMove(1, 0) | |||
| 750 | - | if m.Current() != w || m.overviewTarget() != want || !w.Columns[w.Focus].FullWidth { | |||
| 751 | - | t.Fatalf("right: target %d, want maximized %d", m.overviewTarget(), want) | |||
| 752 | - | } | |||
| 753 | - | } | |||
| 754 | - | m.CancelOverview() | |||
| 755 | - | if focusedID(m) != 1 || !w.Columns[w.Focus].FullWidth { | |||
| 756 | - | t.Fatalf("escape: focus %d, want maximized 1", focusedID(m)) | |||
| 757 | - | } | |||
| 758 | - | } | |||
| 759 | - | ||||
| 760 | // Right from a stash card lands on the maximized column, which keeps its | 470 | // Right from a stash card lands on the maximized column, which keeps its | ||
| 761 | // maximization, not on the first card of its band. | 471 | // maximization, not on the first card of its band. | ||
| 762 | func TestOverviewCascadeStashToMaximized(t *testing.T) { | 472 | func TestOverviewCascadeStashToMaximized(t *testing.T) { | ||
M internal/core/overview_stack.go +1 -5
| @@ -214,11 +214,7 @@ func (m *Monitor) hiddenColumn(w *Workspace) int { | |||
| 214 | 214 | // selectOverviewColumn retains the existing ordinary-row live focus semantics. | |
| 215 | 215 | func (w *Workspace) selectOverviewColumn(i int) { | |
| 216 | 216 | w.floatFocus, w.stashFocus = false, false | |
| 217 | - | if w.policy().wraps && w.hiddenByMaximized(i) { | |
| 218 | - | // A card behind a maximized cascade column takes the | |
| 219 | - | // maximization; Escape gives it back (restoreRow). | |
| 220 | - | w.transferMaximization(i) | |
| 221 | - | } else if w.policy().equalCells && w.Focus != i && w.Focus < len(w.Columns) { | |
| 217 | + | if w.policy().maximizeHides() && w.Focus != i && w.Focus < len(w.Columns) { | |
| 222 | 218 | w.unmaximize() | |
| 223 | 219 | } | |
| 224 | 220 | w.Focus = i | |
| @@ -214,11 +214,7 @@ func (m *Monitor) hiddenColumn(w *Workspace) int { | |||||
| 214 | // selectOverviewColumn retains the existing ordinary-row live focus semantics. | 214 | // selectOverviewColumn retains the existing ordinary-row live focus semantics. | ||
| 215 | func (w *Workspace) selectOverviewColumn(i int) { | 215 | func (w *Workspace) selectOverviewColumn(i int) { | ||
| 216 | w.floatFocus, w.stashFocus = false, false | 216 | w.floatFocus, w.stashFocus = false, false | ||
| 217 | - | if w.policy().wraps && w.hiddenByMaximized(i) { | 217 | + | if w.policy().maximizeHides() && w.Focus != i && w.Focus < len(w.Columns) { |
| 218 | - | // A card behind a maximized cascade column takes the | |||
| 219 | - | // maximization; Escape gives it back (restoreRow). | |||
| 220 | - | w.transferMaximization(i) | |||
| 221 | - | } else if w.policy().equalCells && w.Focus != i && w.Focus < len(w.Columns) { | |||
| 222 | w.unmaximize() | 218 | w.unmaximize() | ||
| 223 | } | 219 | } | ||
| 224 | w.Focus = i | 220 | w.Focus = i | ||
M internal/core/rules.go +1 -1
| @@ -190,7 +190,7 @@ func (w *Workspace) addColumnQuiet(col Column) { | |||
| 190 | 190 | return | |
| 191 | 191 | } | |
| 192 | 192 | if w.policy().equalCells { | |
| 193 | - | if at > 0 { | |
| 193 | + | if at > 0 && w.policy().maximizeHides() { | |
| 194 | 194 | w.unmaximize() | |
| 195 | 195 | } | |
| 196 | 196 | } else if at > 0 { | |
| @@ -190,7 +190,7 @@ func (w *Workspace) addColumnQuiet(col Column) { | |||||
| 190 | return | 190 | return | ||
| 191 | } | 191 | } | ||
| 192 | if w.policy().equalCells { | 192 | if w.policy().equalCells { | ||
| 193 | - | if at > 0 { | 193 | + | if at > 0 && w.policy().maximizeHides() { |
| 194 | w.unmaximize() | 194 | w.unmaximize() | ||
| 195 | } | 195 | } | ||
| 196 | } else if at > 0 { | 196 | } else if at > 0 { | ||
M internal/core/stash.go +1 -1
| @@ -132,7 +132,7 @@ func (w *Workspace) restore(id WindowID, back *origPlace) { | |||
| 132 | 132 | } | |
| 133 | 133 | c.Windows = slices.Insert(c.Windows, row, id) | |
| 134 | 134 | c.Focus = row | |
| 135 | - | if w.policy().equalCells && w.Focus != i { | |
| 135 | + | if w.policy().maximizeHides() && w.Focus != i { | |
| 136 | 136 | w.unmaximize() | |
| 137 | 137 | } | |
| 138 | 138 | w.Focus = i | |
| @@ -132,7 +132,7 @@ func (w *Workspace) restore(id WindowID, back *origPlace) { | |||||
| 132 | } | 132 | } | ||
| 133 | c.Windows = slices.Insert(c.Windows, row, id) | 133 | c.Windows = slices.Insert(c.Windows, row, id) | ||
| 134 | c.Focus = row | 134 | c.Focus = row | ||
| 135 | - | if w.policy().equalCells && w.Focus != i { | 135 | + | if w.policy().maximizeHides() && w.Focus != i { |
| 136 | w.unmaximize() | 136 | w.unmaximize() | ||
| 137 | } | 137 | } | ||
| 138 | w.Focus = i | 138 | w.Focus = i | ||
M internal/core/switcher.go +1 -1
| @@ -165,7 +165,7 @@ func (m *Monitor) commitSwitcher() bool { | |||
| 165 | 165 | if i == w.Focus && !w.floatFocus && !w.stashFocused() { | |
| 166 | 166 | return true | |
| 167 | 167 | } | |
| 168 | - | if i != w.Focus && w.Focus >= 0 && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth { | |
| 168 | + | if i != w.Focus && !w.policy().wraps && w.Focus >= 0 && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth { | |
| 169 | 169 | w.transferMaximization(i) | |
| 170 | 170 | } | |
| 171 | 171 | // Activate leaves another window's fullscreen and handles dialogs. | |
| @@ -165,7 +165,7 @@ func (m *Monitor) commitSwitcher() bool { | |||||
| 165 | if i == w.Focus && !w.floatFocus && !w.stashFocused() { | 165 | if i == w.Focus && !w.floatFocus && !w.stashFocused() { | ||
| 166 | return true | 166 | return true | ||
| 167 | } | 167 | } | ||
| 168 | - | if i != w.Focus && w.Focus >= 0 && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth { | 168 | + | if i != w.Focus && !w.policy().wraps && w.Focus >= 0 && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth { |
| 169 | w.transferMaximization(i) | 169 | w.transferMaximization(i) | ||
| 170 | } | 170 | } | ||
| 171 | // Activate leaves another window's fullscreen and handles dialogs. | 171 | // Activate leaves another window's fullscreen and handles dialogs. | ||
M internal/core/switcher_test.go +7 -0
| @@ -363,6 +363,13 @@ func TestSwitchCommitTransfersMaximization(t *testing.T) { | |||
| 363 | 363 | } | |
| 364 | 364 | openSwitcher(m, 1) | |
| 365 | 365 | m.commitSwitcher() | |
| 366 | + | if overflow == OverflowCascade { | |
| 367 | + | // The maximized column has its own band: it stays. | |
| 368 | + | if id, _ := w.Focused(); w.Columns[w.columnOf(id)].FullWidth || !w.Columns[a].FullWidth { | |
| 369 | + | t.Fatalf("cascade: maximization moved: %+v", w.Columns) | |
| 370 | + | } | |
| 371 | + | return | |
| 372 | + | } | |
| 366 | 373 | if w.Focus != b || !w.Columns[b].FullWidth || w.Columns[a].FullWidth { | |
| 367 | 374 | t.Fatalf("focus %d, FullWidth source %v target %v", w.Focus, w.Columns[a].FullWidth, w.Columns[b].FullWidth) | |
| 368 | 375 | } | |
| @@ -363,6 +363,13 @@ func TestSwitchCommitTransfersMaximization(t *testing.T) { | |||||
| 363 | } | 363 | } | ||
| 364 | openSwitcher(m, 1) | 364 | openSwitcher(m, 1) | ||
| 365 | m.commitSwitcher() | 365 | m.commitSwitcher() | ||
| 366 | + | if overflow == OverflowCascade { | |||
| 367 | + | // The maximized column has its own band: it stays. | |||
| 368 | + | if id, _ := w.Focused(); w.Columns[w.columnOf(id)].FullWidth || !w.Columns[a].FullWidth { | |||
| 369 | + | t.Fatalf("cascade: maximization moved: %+v", w.Columns) | |||
| 370 | + | } | |||
| 371 | + | return | |||
| 372 | + | } | |||
| 366 | if w.Focus != b || !w.Columns[b].FullWidth || w.Columns[a].FullWidth { | 373 | if w.Focus != b || !w.Columns[b].FullWidth || w.Columns[a].FullWidth { | ||
| 367 | t.Fatalf("focus %d, FullWidth source %v target %v", w.Focus, w.Columns[a].FullWidth, w.Columns[b].FullWidth) | 374 | t.Fatalf("focus %d, FullWidth source %v target %v", w.Focus, w.Columns[a].FullWidth, w.Columns[b].FullWidth) | ||
| 368 | } | 375 | } | ||