fix(core): reach the neighbor monitor before the trailing empty cascade workspace
- commit
- 6a8bd4f7234dc79cdecb875b7b03d9676a92bbb8
- author
- brice · · 2026-10-12 14:07 UTC
- parent
- 6b64d3c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +57 -30
M docs/config.md +3 -3
| @@ -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 to the previous or next workspace, so the opposite key comes back; past the first or the trailing empty workspace, to the neighbor monitor on that side; `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 to the previous or next occupied workspace, so the opposite key comes back; 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. | |
| 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 | 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. | |
| 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 workspace at the band's edge, then to the neighbor monitor). 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. 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. | |
| 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 | ||
| @@ -267,7 +267,7 @@ In a row, the auto columns share the cells the set ones leave; when no cell is l | |||
| 267 | 267 | | `switch-column-prev` | The same, in the opposite direction | | |
| 268 | 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 | | |
| 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 | - | | `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 workspace, then, past the first or the trailing empty one, the neighbor monitor. 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 | 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 | | |
| 272 | 272 | | `move-column-left/right` | Move the focused column; at the edge, to the neighbor monitor, focus following. In cascade overflow the edge is the band's; with no monitor there, the column crosses into the adjacent band. Fixed overflow follows column order, not the screen | | |
| 273 | 273 | | `move-window-up/down` | Move the focused window up or down in its column; focus follows it. In cascade overflow, past the column, the column moves to the adjacent band, where `focus-window-up/down` goes | | |
| @@ -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 to the previous or next workspace, so the opposite key comes back; past the first or the trailing empty workspace, to the neighbor monitor on that side; `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 to the previous or next occupied workspace, so the opposite key comes back; 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. |
| 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. 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. | ||
| 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 workspace at the band's edge, then to the neighbor monitor). 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. 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. |
| 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 | ||||
| @@ -267,7 +267,7 @@ In a row, the auto columns share the cells the set ones leave; when no cell is l | |||||
| 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. 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 workspace, then, past the first or the trailing empty one, the neighbor monitor. 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 | | ||
| 272 | | `move-column-left/right` | Move the focused column; at the edge, to the neighbor monitor, focus following. In cascade overflow the edge is the band's; with no monitor there, the column crosses into the adjacent band. Fixed overflow follows column order, not the screen | | 272 | | `move-column-left/right` | Move the focused column; at the edge, to the neighbor monitor, focus following. In cascade overflow the edge is the band's; with no monitor there, the column crosses into the adjacent band. Fixed overflow follows column order, not the screen | | ||
| 273 | | `move-window-up/down` | Move the focused window up or down in its column; focus follows it. In cascade overflow, past the column, the column moves to the adjacent band, where `focus-window-up/down` goes | | 273 | | `move-window-up/down` | Move the focused window up or down in its column; focus follows it. In cascade overflow, past the column, the column moves to the adjacent band, where `focus-window-up/down` goes | | ||
M internal/core/action.go +24 -19
| @@ -230,31 +230,20 @@ func (c *Core) applyAction(a Action) Effect { | |||
| 230 | 230 | dir, d = -1, dirLeft | |
| 231 | 231 | } | |
| 232 | 232 | // A covering fullscreen window is left for the window on that side; | |
| 233 | - | // with none (the workspace, band or stash edge), the move goes along | |
| 234 | - | // the workspace axis, else to the neighbor monitor, and fullscreen | |
| 235 | - | // stays. | |
| 233 | + | // with none (the workspace, band or stash edge), the move goes past | |
| 234 | + | // the edge (see edgeFocus) and fullscreen stays. | |
| 236 | 235 | if w.pinned() && !w.stashOverCover() { | |
| 237 | - | if !w.FocusColumn(dir) && !c.cur().mon.stepWorkspace(horizontalAxis, dir) { | |
| 238 | - | if i := c.neighbor(d); i >= 0 { | |
| 239 | - | c.focusScreen = i | |
| 240 | - | } | |
| 236 | + | if !w.FocusColumn(dir) { | |
| 237 | + | c.edgeFocus(dir, d) | |
| 241 | 238 | } | |
| 242 | 239 | return Effect{} | |
| 243 | 240 | } | |
| 244 | 241 | // The first move from a float stays here unless nothing is under it: | |
| 245 | 242 | // a native float leaves for the stash or the columns, and the stash | |
| 246 | - | // keeps the focus at its ends. At the edge the move goes along the | |
| 247 | - | // workspace axis if it is horizontal, so the way back is the same | |
| 248 | - | // key; past the first or trailing workspace, to the neighbor monitor. | |
| 243 | + | // keeps the focus at its ends. | |
| 249 | 244 | edge := w.leavesBand(w.columnToward(dir)) && (!w.onFloat() || w.floatFocus && !w.canLeaveFloat()) | |
| 250 | - | if edge { | |
| 251 | - | if c.cur().mon.stepWorkspace(horizontalAxis, dir) { | |
| 252 | - | return Effect{} | |
| 253 | - | } | |
| 254 | - | if i := c.neighbor(d); i >= 0 { | |
| 255 | - | c.focusScreen = i | |
| 256 | - | return Effect{} | |
| 257 | - | } | |
| 245 | + | if edge && c.edgeFocus(dir, d) { | |
| 246 | + | return Effect{} | |
| 258 | 247 | } | |
| 259 | 248 | case ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight: | |
| 260 | 249 | dir, d := 1, dirRight | |
| @@ -311,6 +300,22 @@ func (c *Core) applyAction(a Action) Effect { | |||
| 311 | 300 | return c.cur().mon.Apply(a) | |
| 312 | 301 | } | |
| 313 | 302 | ||
| 303 | + | // edgeFocus moves focus-column-left/right (dir -1/1, toward d) past the | |
| 304 | + | // content edge: to the occupied workspace on that side when the workspace | |
| 305 | + | // axis is horizontal, so the opposite key comes back; else to the neighbor | |
| 306 | + | // monitor; else to the trailing empty workspace. It reports whether focus | |
| 307 | + | // moved. | |
| 308 | + | func (c *Core) edgeFocus(dir int, d direction) bool { | |
| 309 | + | if c.cur().mon.stepWorkspace(horizontalAxis, dir, false) { | |
| 310 | + | return true | |
| 311 | + | } | |
| 312 | + | if i := c.neighbor(d); i >= 0 { | |
| 313 | + | c.focusScreen = i | |
| 314 | + | return true | |
| 315 | + | } | |
| 316 | + | return c.cur().mon.stepWorkspace(horizontalAxis, dir, true) | |
| 317 | + | } | |
| 318 | + | ||
| 314 | 319 | // Apply runs a bind action on the monitor. | |
| 315 | 320 | func (m *Monitor) Apply(a Action) Effect { | |
| 316 | 321 | if e, ok := m.overviewAction(a); ok { | |
| @@ -349,7 +354,7 @@ func (m *Monitor) Apply(a Action) Effect { | |||
| 349 | 354 | dir = -1 | |
| 350 | 355 | } | |
| 351 | 356 | if !m.Current().FocusWindow(dir) { | |
| 352 | - | m.stepWorkspace(verticalAxis, dir) | |
| 357 | + | m.stepWorkspace(verticalAxis, dir, true) | |
| 353 | 358 | } | |
| 354 | 359 | return Effect{} | |
| 355 | 360 | case ActionToggleFullscreen: | |
| @@ -230,31 +230,20 @@ func (c *Core) applyAction(a Action) Effect { | |||||
| 230 | dir, d = -1, dirLeft | 230 | dir, d = -1, dirLeft | ||
| 231 | } | 231 | } | ||
| 232 | // A covering fullscreen window is left for the window on that side; | 232 | // A covering fullscreen window is left for the window on that side; | ||
| 233 | - | // with none (the workspace, band or stash edge), the move goes along | 233 | + | // with none (the workspace, band or stash edge), the move goes past |
| 234 | - | // the workspace axis, else to the neighbor monitor, and fullscreen | 234 | + | // the edge (see edgeFocus) and fullscreen stays. |
| 235 | - | // stays. | |||
| 236 | if w.pinned() && !w.stashOverCover() { | 235 | if w.pinned() && !w.stashOverCover() { | ||
| 237 | - | if !w.FocusColumn(dir) && !c.cur().mon.stepWorkspace(horizontalAxis, dir) { | 236 | + | if !w.FocusColumn(dir) { |
| 238 | - | if i := c.neighbor(d); i >= 0 { | 237 | + | c.edgeFocus(dir, d) |
| 239 | - | c.focusScreen = i | |||
| 240 | - | } | |||
| 241 | } | 238 | } | ||
| 242 | return Effect{} | 239 | return Effect{} | ||
| 243 | } | 240 | } | ||
| 244 | // The first move from a float stays here unless nothing is under it: | 241 | // The first move from a float stays here unless nothing is under it: | ||
| 245 | // a native float leaves for the stash or the columns, and the stash | 242 | // a native float leaves for the stash or the columns, and the stash | ||
| 246 | - | // keeps the focus at its ends. At the edge the move goes along the | 243 | + | // keeps the focus at its ends. |
| 247 | - | // workspace axis if it is horizontal, so the way back is the same | |||
| 248 | - | // key; past the first or trailing workspace, to the neighbor monitor. | |||
| 249 | edge := w.leavesBand(w.columnToward(dir)) && (!w.onFloat() || w.floatFocus && !w.canLeaveFloat()) | 244 | edge := w.leavesBand(w.columnToward(dir)) && (!w.onFloat() || w.floatFocus && !w.canLeaveFloat()) | ||
| 250 | - | if edge { | 245 | + | if edge && c.edgeFocus(dir, d) { |
| 251 | - | if c.cur().mon.stepWorkspace(horizontalAxis, dir) { | 246 | + | return Effect{} |
| 252 | - | return Effect{} | |||
| 253 | - | } | |||
| 254 | - | if i := c.neighbor(d); i >= 0 { | |||
| 255 | - | c.focusScreen = i | |||
| 256 | - | return Effect{} | |||
| 257 | - | } | |||
| 258 | } | 247 | } | ||
| 259 | case ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight: | 248 | case ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight: | ||
| 260 | dir, d := 1, dirRight | 249 | dir, d := 1, dirRight | ||
| @@ -311,6 +300,22 @@ func (c *Core) applyAction(a Action) Effect { | |||||
| 311 | return c.cur().mon.Apply(a) | 300 | return c.cur().mon.Apply(a) | ||
| 312 | } | 301 | } | ||
| 313 | 302 | ||||
| 303 | + | // edgeFocus moves focus-column-left/right (dir -1/1, toward d) past the | |||
| 304 | + | // content edge: to the occupied workspace on that side when the workspace | |||
| 305 | + | // axis is horizontal, so the opposite key comes back; else to the neighbor | |||
| 306 | + | // monitor; else to the trailing empty workspace. It reports whether focus | |||
| 307 | + | // moved. | |||
| 308 | + | func (c *Core) edgeFocus(dir int, d direction) bool { | |||
| 309 | + | if c.cur().mon.stepWorkspace(horizontalAxis, dir, false) { | |||
| 310 | + | return true | |||
| 311 | + | } | |||
| 312 | + | if i := c.neighbor(d); i >= 0 { | |||
| 313 | + | c.focusScreen = i | |||
| 314 | + | return true | |||
| 315 | + | } | |||
| 316 | + | return c.cur().mon.stepWorkspace(horizontalAxis, dir, true) | |||
| 317 | + | } | |||
| 318 | + | ||||
| 314 | // Apply runs a bind action on the monitor. | 319 | // Apply runs a bind action on the monitor. | ||
| 315 | func (m *Monitor) Apply(a Action) Effect { | 320 | func (m *Monitor) Apply(a Action) Effect { | ||
| 316 | if e, ok := m.overviewAction(a); ok { | 321 | if e, ok := m.overviewAction(a); ok { | ||
| @@ -349,7 +354,7 @@ func (m *Monitor) Apply(a Action) Effect { | |||||
| 349 | dir = -1 | 354 | dir = -1 | ||
| 350 | } | 355 | } | ||
| 351 | if !m.Current().FocusWindow(dir) { | 356 | if !m.Current().FocusWindow(dir) { | ||
| 352 | - | m.stepWorkspace(verticalAxis, dir) | 357 | + | m.stepWorkspace(verticalAxis, dir, true) |
| 353 | } | 358 | } | ||
| 354 | return Effect{} | 359 | return Effect{} | ||
| 355 | case ActionToggleFullscreen: | 360 | case ActionToggleFullscreen: | ||
M internal/core/monitor.go +7 -3
| @@ -207,14 +207,18 @@ func (m *Monitor) Focus(i int) { | |||
| 207 | 207 | ||
| 208 | 208 | // stepWorkspace switches to the previous or next workspace (dir -1/1) when | |
| 209 | 209 | // axis is the current layout's workspace axis, and reports whether it did. | |
| 210 | - | // Past the first or the trailing workspace there is none to step to. A | |
| 211 | - | // hidden named workspace is outside the numbered list: it never steps. | |
| 212 | - | func (m *Monitor) stepWorkspace(axis layoutAxis, dir int) bool { | |
| 210 | + | // Past the first or the trailing workspace there is none to step to; the | |
| 211 | + | // trailing empty workspace counts only with empty set. A hidden named | |
| 212 | + | // workspace is outside the numbered list: it never steps. | |
| 213 | + | func (m *Monitor) stepWorkspace(axis layoutAxis, dir int, empty bool) bool { | |
| 213 | 214 | step := m.Current().policy().workspaceStep(axis, dir) | |
| 214 | 215 | to := m.Active + step | |
| 215 | 216 | if m.shown != nil || step == 0 || to < 0 || to >= len(m.Workspaces) { | |
| 216 | 217 | return false | |
| 217 | 218 | } | |
| 219 | + | if w := m.Workspaces[to]; !empty && w.empty() && w.Name == "" { | |
| 220 | + | return false | |
| 221 | + | } | |
| 218 | 222 | m.Focus(to) | |
| 219 | 223 | return true | |
| 220 | 224 | } | |
| @@ -207,14 +207,18 @@ func (m *Monitor) Focus(i int) { | |||||
| 207 | 207 | ||||
| 208 | // stepWorkspace switches to the previous or next workspace (dir -1/1) when | 208 | // stepWorkspace switches to the previous or next workspace (dir -1/1) when | ||
| 209 | // axis is the current layout's workspace axis, and reports whether it did. | 209 | // axis is the current layout's workspace axis, and reports whether it did. | ||
| 210 | - | // Past the first or the trailing workspace there is none to step to. A | 210 | + | // Past the first or the trailing workspace there is none to step to; the |
| 211 | - | // hidden named workspace is outside the numbered list: it never steps. | 211 | + | // trailing empty workspace counts only with empty set. A hidden named |
| 212 | - | func (m *Monitor) stepWorkspace(axis layoutAxis, dir int) bool { | 212 | + | // workspace is outside the numbered list: it never steps. |
| 213 | + | func (m *Monitor) stepWorkspace(axis layoutAxis, dir int, empty bool) bool { | |||
| 213 | step := m.Current().policy().workspaceStep(axis, dir) | 214 | step := m.Current().policy().workspaceStep(axis, dir) | ||
| 214 | to := m.Active + step | 215 | to := m.Active + step | ||
| 215 | if m.shown != nil || step == 0 || to < 0 || to >= len(m.Workspaces) { | 216 | if m.shown != nil || step == 0 || to < 0 || to >= len(m.Workspaces) { | ||
| 216 | return false | 217 | return false | ||
| 217 | } | 218 | } | ||
| 219 | + | if w := m.Workspaces[to]; !empty && w.empty() && w.Name == "" { | |||
| 220 | + | return false | |||
| 221 | + | } | |||
| 218 | m.Focus(to) | 222 | m.Focus(to) | ||
| 219 | return true | 223 | return true | ||
| 220 | } | 224 | } | ||
M internal/core/workspace_axis_test.go +23 -5
| @@ -50,9 +50,9 @@ func TestCascadeFocusColumnStepsWorkspaceAtBandEdge(t *testing.T) { | |||
| 50 | 50 | } | |
| 51 | 51 | } | |
| 52 | 52 | ||
| 53 | - | // The workspace step wins over a monitor on that side, so the same key | |
| 54 | - | // brings focus back: a neighbor monitor is reached only past the first or | |
| 55 | - | // the trailing workspace. | |
| 53 | + | // An occupied workspace wins over a monitor on that side, so the opposite | |
| 54 | + | // key brings focus back; the monitor wins over the trailing empty | |
| 55 | + | // workspace, so it stays reachable. | |
| 56 | 56 | func TestCascadeFocusColumnStepsWorkspaceBeforeMonitor(t *testing.T) { | |
| 57 | 57 | c, m := axisCore(t, true, OverflowCascade, 3) | |
| 58 | 58 | m.Current().FocusID(23) | |
| @@ -74,11 +74,29 @@ func TestCascadeFocusColumnStepsWorkspaceBeforeMonitor(t *testing.T) { | |||
| 74 | 74 | c.applyAction(ActionFocusColumnLeft) | |
| 75 | 75 | wantFocus(t, c, "L", 0) | |
| 76 | 76 | wantActive(t, m, 0) | |
| 77 | - | // Past the trailing empty workspace: the monitor on the right. | |
| 77 | + | // From the last occupied workspace: the monitor on the right, not the | |
| 78 | + | // trailing empty workspace. | |
| 78 | 79 | c.focusScreen = 1 | |
| 79 | - | m.Focus(len(m.Workspaces) - 1) | |
| 80 | + | m.Focus(2) | |
| 81 | + | m.Current().FocusID(35) | |
| 80 | 82 | c.applyAction(ActionFocusColumnRight) | |
| 81 | 83 | wantFocus(t, c, "R", 0) | |
| 84 | + | wantActive(t, m, 2) | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | // With no monitor on that side, the trailing empty workspace is the last | |
| 88 | + | // stop, and left comes back from it. | |
| 89 | + | func TestCascadeFocusColumnReachesTrailingWorkspaceWithoutMonitor(t *testing.T) { | |
| 90 | + | c, m := axisCore(t, false, OverflowCascade, 3) | |
| 91 | + | m.Focus(2) | |
| 92 | + | m.Current().FocusID(35) | |
| 93 | + | c.applyAction(ActionFocusColumnRight) | |
| 94 | + | wantActive(t, m, 3) | |
| 95 | + | if !m.Current().empty() { | |
| 96 | + | t.Fatal("want the trailing empty workspace") | |
| 97 | + | } | |
| 98 | + | c.applyAction(ActionFocusColumnLeft) | |
| 99 | + | wantActive(t, m, 2) | |
| 82 | 100 | } | |
| 83 | 101 | ||
| 84 | 102 | // The live regression: one monitor on the left only. Right steps to the | |
| @@ -50,9 +50,9 @@ func TestCascadeFocusColumnStepsWorkspaceAtBandEdge(t *testing.T) { | |||||
| 50 | } | 50 | } | ||
| 51 | } | 51 | } | ||
| 52 | 52 | ||||
| 53 | - | // The workspace step wins over a monitor on that side, so the same key | 53 | + | // An occupied workspace wins over a monitor on that side, so the opposite |
| 54 | - | // brings focus back: a neighbor monitor is reached only past the first or | 54 | + | // key brings focus back; the monitor wins over the trailing empty |
| 55 | - | // the trailing workspace. | 55 | + | // workspace, so it stays reachable. |
| 56 | func TestCascadeFocusColumnStepsWorkspaceBeforeMonitor(t *testing.T) { | 56 | func TestCascadeFocusColumnStepsWorkspaceBeforeMonitor(t *testing.T) { | ||
| 57 | c, m := axisCore(t, true, OverflowCascade, 3) | 57 | c, m := axisCore(t, true, OverflowCascade, 3) | ||
| 58 | m.Current().FocusID(23) | 58 | m.Current().FocusID(23) | ||
| @@ -74,11 +74,29 @@ func TestCascadeFocusColumnStepsWorkspaceBeforeMonitor(t *testing.T) { | |||||
| 74 | c.applyAction(ActionFocusColumnLeft) | 74 | c.applyAction(ActionFocusColumnLeft) | ||
| 75 | wantFocus(t, c, "L", 0) | 75 | wantFocus(t, c, "L", 0) | ||
| 76 | wantActive(t, m, 0) | 76 | wantActive(t, m, 0) | ||
| 77 | - | // Past the trailing empty workspace: the monitor on the right. | 77 | + | // From the last occupied workspace: the monitor on the right, not the |
| 78 | + | // trailing empty workspace. | |||
| 78 | c.focusScreen = 1 | 79 | c.focusScreen = 1 | ||
| 79 | - | m.Focus(len(m.Workspaces) - 1) | 80 | + | m.Focus(2) |
| 81 | + | m.Current().FocusID(35) | |||
| 80 | c.applyAction(ActionFocusColumnRight) | 82 | c.applyAction(ActionFocusColumnRight) | ||
| 81 | wantFocus(t, c, "R", 0) | 83 | wantFocus(t, c, "R", 0) | ||
| 84 | + | wantActive(t, m, 2) | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | // With no monitor on that side, the trailing empty workspace is the last | |||
| 88 | + | // stop, and left comes back from it. | |||
| 89 | + | func TestCascadeFocusColumnReachesTrailingWorkspaceWithoutMonitor(t *testing.T) { | |||
| 90 | + | c, m := axisCore(t, false, OverflowCascade, 3) | |||
| 91 | + | m.Focus(2) | |||
| 92 | + | m.Current().FocusID(35) | |||
| 93 | + | c.applyAction(ActionFocusColumnRight) | |||
| 94 | + | wantActive(t, m, 3) | |||
| 95 | + | if !m.Current().empty() { | |||
| 96 | + | t.Fatal("want the trailing empty workspace") | |||
| 97 | + | } | |||
| 98 | + | c.applyAction(ActionFocusColumnLeft) | |||
| 99 | + | wantActive(t, m, 2) | |||
| 82 | } | 100 | } | ||
| 83 | 101 | ||||
| 84 | // The live regression: one monitor on the left only. Right steps to the | 102 | // The live regression: one monitor on the left only. Right steps to the | ||