fix(overview): select the stack of a workspace stepped to when its stash carousel wraps round
- commit
- 3a090b4dc3f18bc582049784e0f6246eeb9465db
- author
- brice · · 2026-10-16 13:40 UTC
- parent
- 6648883
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +49 -2
M docs/config.md +1 -1
| @@ -276,7 +276,7 @@ The window must appear within 5 seconds, otherwise it opens normally. Apps that | |||
| 276 | 276 | ||
| 277 | 277 | `toggle-overview` (or a four-finger swipe up) shows previews of your workspaces. Arrows, `h/j/k/l` or three-finger swipes move the selection; `Return` or a click opens it; `Escape` goes back. `close-window` and the move-to-workspace binds work on the selected preview. | |
| 278 | 278 | ||
| 279 | - | A workspace's stash shows as a pile of cards. Selecting the pile unfolds it into a carousel of every stashed window: a row under the workspace with `layout.overflow = cascade`, a column on its left otherwise. The selected card stays in the middle, the earlier cards before it and the later ones after it, wrapping round; a wider gap separates the newest card from the oldest. Moves along the carousel pick a card. With fewer than three cards, or on a workspace holding only stashed windows, it does not wrap round. Leaving it folds it again. | |
| 279 | + | A workspace's stash shows as a pile of cards. Selecting the pile unfolds it into a carousel of every stashed window: a row under the workspace with `layout.overflow = cascade`, a column on its left otherwise. The selected card stays in the middle, the earlier cards before it and the later ones after it, wrapping round; a wider gap separates the newest card from the oldest. Moves along the carousel pick a card. With fewer than three cards, or on a workspace holding only stashed windows, it does not wrap round. Leaving it folds it again. Moving to another workspace selects its windows rather than a carousel that wraps round, even when its stash has the focus, so browsing workspaces never stops on one. | |
| 280 | 280 | ||
| 281 | 281 | With `layout.overflow = cascade`, workspaces sit side by side, each band of columns is a lane, and the stash sits under the lanes. Down (or fingers down on the touchpad, with natural scroll off) moves to the lane below; past the last lane it unfolds the stash. Left/right walk the cards; where the carousel does not wrap round, they go to the neighbor workspace past either end. Up leaves the stash for the last lane, at the column you came from. | |
| 282 | 282 | ||
| @@ -276,7 +276,7 @@ The window must appear within 5 seconds, otherwise it opens normally. Apps that | |||||
| 276 | 276 | ||||
| 277 | `toggle-overview` (or a four-finger swipe up) shows previews of your workspaces. Arrows, `h/j/k/l` or three-finger swipes move the selection; `Return` or a click opens it; `Escape` goes back. `close-window` and the move-to-workspace binds work on the selected preview. | 277 | `toggle-overview` (or a four-finger swipe up) shows previews of your workspaces. Arrows, `h/j/k/l` or three-finger swipes move the selection; `Return` or a click opens it; `Escape` goes back. `close-window` and the move-to-workspace binds work on the selected preview. | ||
| 278 | 278 | ||||
| 279 | - | A workspace's stash shows as a pile of cards. Selecting the pile unfolds it into a carousel of every stashed window: a row under the workspace with `layout.overflow = cascade`, a column on its left otherwise. The selected card stays in the middle, the earlier cards before it and the later ones after it, wrapping round; a wider gap separates the newest card from the oldest. Moves along the carousel pick a card. With fewer than three cards, or on a workspace holding only stashed windows, it does not wrap round. Leaving it folds it again. | 279 | + | A workspace's stash shows as a pile of cards. Selecting the pile unfolds it into a carousel of every stashed window: a row under the workspace with `layout.overflow = cascade`, a column on its left otherwise. The selected card stays in the middle, the earlier cards before it and the later ones after it, wrapping round; a wider gap separates the newest card from the oldest. Moves along the carousel pick a card. With fewer than three cards, or on a workspace holding only stashed windows, it does not wrap round. Leaving it folds it again. Moving to another workspace selects its windows rather than a carousel that wraps round, even when its stash has the focus, so browsing workspaces never stops on one. |
| 280 | 280 | ||||
| 281 | With `layout.overflow = cascade`, workspaces sit side by side, each band of columns is a lane, and the stash sits under the lanes. Down (or fingers down on the touchpad, with natural scroll off) moves to the lane below; past the last lane it unfolds the stash. Left/right walk the cards; where the carousel does not wrap round, they go to the neighbor workspace past either end. Up leaves the stash for the last lane, at the column you came from. | 281 | With `layout.overflow = cascade`, workspaces sit side by side, each band of columns is a lane, and the stash sits under the lanes. Down (or fingers down on the touchpad, with natural scroll off) moves to the lane below; past the last lane it unfolds the stash. Left/right walk the cards; where the carousel does not wrap round, they go to the neighbor workspace past either end. Up leaves the stash for the last lane, at the column you came from. | ||
| 282 | 282 | ||||
M internal/core/overview.go +6 -1
| @@ -554,12 +554,17 @@ func (m *Monitor) overviewVertical(dy int) { | |||
| 554 | 554 | } | |
| 555 | 555 | ||
| 556 | 556 | // overviewStep shows the workspace d rows from the current one; it stays | |
| 557 | - | // on the first or last row. | |
| 557 | + | // on the first or last row. The row selects its stack even when its stash | |
| 558 | + | // has the focus, if the stash wraps round (wrapsCards): the moves that | |
| 559 | + | // stepped there would only walk the carousel, never reaching the next row. | |
| 558 | 560 | func (m *Monitor) overviewStep(d int) { | |
| 559 | 561 | rows := m.overviewWorkspaces() | |
| 560 | 562 | i := indexOf(rows, m.Current()) + d | |
| 561 | 563 | if i >= 0 && i < len(rows) { | |
| 562 | 564 | m.showOverview(rows[i]) | |
| 565 | + | if w := m.Current(); m.cardAt(w) >= 0 && w.wrapsCards() { | |
| 566 | + | m.clearCard() | |
| 567 | + | } | |
| 563 | 568 | } | |
| 564 | 569 | } | |
| 565 | 570 | ||
| @@ -554,12 +554,17 @@ func (m *Monitor) overviewVertical(dy int) { | |||||
| 554 | } | 554 | } | ||
| 555 | 555 | ||||
| 556 | // overviewStep shows the workspace d rows from the current one; it stays | 556 | // overviewStep shows the workspace d rows from the current one; it stays | ||
| 557 | - | // on the first or last row. | 557 | + | // on the first or last row. The row selects its stack even when its stash |
| 558 | + | // has the focus, if the stash wraps round (wrapsCards): the moves that | |||
| 559 | + | // stepped there would only walk the carousel, never reaching the next row. | |||
| 558 | func (m *Monitor) overviewStep(d int) { | 560 | func (m *Monitor) overviewStep(d int) { | ||
| 559 | rows := m.overviewWorkspaces() | 561 | rows := m.overviewWorkspaces() | ||
| 560 | i := indexOf(rows, m.Current()) + d | 562 | i := indexOf(rows, m.Current()) + d | ||
| 561 | if i >= 0 && i < len(rows) { | 563 | if i >= 0 && i < len(rows) { | ||
| 562 | m.showOverview(rows[i]) | 564 | m.showOverview(rows[i]) | ||
| 565 | + | if w := m.Current(); m.cardAt(w) >= 0 && w.wrapsCards() { | |||
| 566 | + | m.clearCard() | |||
| 567 | + | } | |||
| 563 | } | 568 | } | ||
| 564 | } | 569 | } | ||
| 565 | 570 | ||||
M internal/core/overview_unfold_test.go +42 -0
| @@ -359,3 +359,45 @@ func TestOverviewStashOnlyCarouselScrolls(t *testing.T) { | |||
| 359 | 359 | t.Fatalf("last card ends at %d, area at %d", r.X+r.W, u.X+u.W) | |
| 360 | 360 | } | |
| 361 | 361 | } | |
| 362 | + | ||
| 363 | + | // Stepping to a workspace whose stash has the focus selects its stack when | |
| 364 | + | // the carousel wraps round, so the next step goes on to the next workspace; | |
| 365 | + | // a carousel that does not wrap is still selected. | |
| 366 | + | func TestOverviewStepSkipsWrappingCarousel(t *testing.T) { | |
| 367 | + | for _, overflow := range []Overflow{OverflowCascade, OverflowScroll} { | |
| 368 | + | for _, n := range []int{2, 3} { | |
| 369 | + | m := overviewMonitor() | |
| 370 | + | m.Focus(2) | |
| 371 | + | m.AddWindow(30) | |
| 372 | + | m.Focus(1) | |
| 373 | + | w := m.Current() | |
| 374 | + | for id := WindowID(40); id < WindowID(40+n); id++ { | |
| 375 | + | w.AddWindow(id) | |
| 376 | + | w.FocusID(id) | |
| 377 | + | w.ToggleWindowStash() | |
| 378 | + | } | |
| 379 | + | for _, ws := range m.Workspaces[:3] { | |
| 380 | + | ws.Overflow = overflow | |
| 381 | + | } | |
| 382 | + | m.Focus(0) | |
| 383 | + | if !w.stashFocused() { | |
| 384 | + | t.Fatal("setup: the stash does not have the focus") | |
| 385 | + | } | |
| 386 | + | m.ToggleOverview() | |
| 387 | + | m.overviewFocus(ActionFocusWorkspaceNext) | |
| 388 | + | if m.Current() != w { | |
| 389 | + | t.Fatalf("%s, %d cards: on workspace %d", overflow, n, indexOf(m.Workspaces, m.Current())) | |
| 390 | + | } | |
| 391 | + | if got, want := m.cardAt(w) >= 0, n < 3; got != want { | |
| 392 | + | t.Fatalf("%s, %d cards: card selected %v, want %v", overflow, n, got, want) | |
| 393 | + | } | |
| 394 | + | if n < 3 { | |
| 395 | + | continue | |
| 396 | + | } | |
| 397 | + | m.overviewFocus(ActionFocusWorkspaceNext) | |
| 398 | + | if m.Current() != m.Workspaces[2] { | |
| 399 | + | t.Fatalf("%s: the next step stayed on workspace %d", overflow, indexOf(m.Workspaces, m.Current())) | |
| 400 | + | } | |
| 401 | + | } | |
| 402 | + | } | |
| 403 | + | } | |
| @@ -359,3 +359,45 @@ func TestOverviewStashOnlyCarouselScrolls(t *testing.T) { | |||||
| 359 | t.Fatalf("last card ends at %d, area at %d", r.X+r.W, u.X+u.W) | 359 | t.Fatalf("last card ends at %d, area at %d", r.X+r.W, u.X+u.W) | ||
| 360 | } | 360 | } | ||
| 361 | } | 361 | } | ||
| 362 | + | ||||
| 363 | + | // Stepping to a workspace whose stash has the focus selects its stack when | |||
| 364 | + | // the carousel wraps round, so the next step goes on to the next workspace; | |||
| 365 | + | // a carousel that does not wrap is still selected. | |||
| 366 | + | func TestOverviewStepSkipsWrappingCarousel(t *testing.T) { | |||
| 367 | + | for _, overflow := range []Overflow{OverflowCascade, OverflowScroll} { | |||
| 368 | + | for _, n := range []int{2, 3} { | |||
| 369 | + | m := overviewMonitor() | |||
| 370 | + | m.Focus(2) | |||
| 371 | + | m.AddWindow(30) | |||
| 372 | + | m.Focus(1) | |||
| 373 | + | w := m.Current() | |||
| 374 | + | for id := WindowID(40); id < WindowID(40+n); id++ { | |||
| 375 | + | w.AddWindow(id) | |||
| 376 | + | w.FocusID(id) | |||
| 377 | + | w.ToggleWindowStash() | |||
| 378 | + | } | |||
| 379 | + | for _, ws := range m.Workspaces[:3] { | |||
| 380 | + | ws.Overflow = overflow | |||
| 381 | + | } | |||
| 382 | + | m.Focus(0) | |||
| 383 | + | if !w.stashFocused() { | |||
| 384 | + | t.Fatal("setup: the stash does not have the focus") | |||
| 385 | + | } | |||
| 386 | + | m.ToggleOverview() | |||
| 387 | + | m.overviewFocus(ActionFocusWorkspaceNext) | |||
| 388 | + | if m.Current() != w { | |||
| 389 | + | t.Fatalf("%s, %d cards: on workspace %d", overflow, n, indexOf(m.Workspaces, m.Current())) | |||
| 390 | + | } | |||
| 391 | + | if got, want := m.cardAt(w) >= 0, n < 3; got != want { | |||
| 392 | + | t.Fatalf("%s, %d cards: card selected %v, want %v", overflow, n, got, want) | |||
| 393 | + | } | |||
| 394 | + | if n < 3 { | |||
| 395 | + | continue | |||
| 396 | + | } | |||
| 397 | + | m.overviewFocus(ActionFocusWorkspaceNext) | |||
| 398 | + | if m.Current() != m.Workspaces[2] { | |||
| 399 | + | t.Fatalf("%s: the next step stayed on workspace %d", overflow, indexOf(m.Workspaces, m.Current())) | |||
| 400 | + | } | |||
| 401 | + | } | |||
| 402 | + | } | |||
| 403 | + | } | |||