feat(overview): unfold the vertical overview stash as a carousel too
- commit
- a82be5b47b901cb81ecae00734ad7fc3e4bc7b30
- author
- brice · · 2026-10-16 12:37 UTC
- parent
- 1c7d6c1
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +245 -209
M docs/config.md +3 -3
| @@ -276,8 +276,8 @@ 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, so you see every stashed window: a carousel under the workspace with `layout.overflow = cascade`, a column on its left otherwise. The carousel keeps the selected card in the middle, the earlier cards on its left and the later ones on its right, wrapping round; a wider gap separates the newest card from the oldest. Moves along the unfolded pile pick a card; a pile longer than the screen scrolls with the selection. 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. | |
| 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 round the carousel; with fewer than three cards, or on a workspace holding only stashed windows, 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 | ||
| 283 | - | Otherwise, left from the first column unfolds the stash. Up/down walk the cards and right goes back to the columns. | |
| 283 | + | Otherwise, left from the first column unfolds the stash. Up/down walk the cards, the focus-workspace binds change workspace, and right goes back to the columns. | |
| @@ -276,8 +276,8 @@ 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, so you see every stashed window: a carousel under the workspace with `layout.overflow = cascade`, a column on its left otherwise. The carousel keeps the selected card in the middle, the earlier cards on its left and the later ones on its right, wrapping round; a wider gap separates the newest card from the oldest. Moves along the unfolded pile pick a card; a pile longer than the screen scrolls with the selection. 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. |
| 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 round the carousel; with fewer than three cards, or on a workspace holding only stashed windows, 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 | ||||
| 283 | - | Otherwise, left from the first column unfolds the stash. Up/down walk the cards and right goes back to the columns. | 283 | + | Otherwise, left from the first column unfolds the stash. Up/down walk the cards, the focus-workspace binds change workspace, and right goes back to the columns. |
M internal/core/overview.go +77 -94
| @@ -57,9 +57,8 @@ type rowSnapshot struct { | |||
| 57 | 57 | // (not selected), the stash's own selection is in front at | |
| 58 | 58 | // overviewCardZoom, up to overviewCards-1 others behind it, each | |
| 59 | 59 | // overviewCardStep further up and right, dimmed. Selected, the pile | |
| 60 | - | // unfolds along the overview axis (unfold), the selection framed, and the | |
| 61 | - | // moves along that axis walk it: a column of every card in a vertical | |
| 62 | - | // overview, a carousel centred on the selection in a horizontal one. A vertical | |
| 60 | + | // unfolds along the overview axis into a carousel centred on the selection | |
| 61 | + | // (unfold), framed, and the moves along that axis walk it round. A vertical | |
| 63 | 62 | // overview pins the pile to the left edge of its row, which takes the width | |
| 64 | 63 | // the pile leaves. A horizontal one centres it on its row, in a band | |
| 65 | 64 | // reserved under the selected lane (pileBand): the lane and the band are | |
| @@ -159,15 +158,6 @@ func (m *Monitor) cardAt(w *Workspace) int { | |||
| 159 | 158 | return -1 | |
| 160 | 159 | } | |
| 161 | 160 | ||
| 162 | - | // carouselCard is the selected card of w's stash when it unfolds as a | |
| 163 | - | // carousel (a horizontal overview), -1 otherwise. | |
| 164 | - | func (m *Monitor) carouselCard(w *Workspace) int { | |
| 165 | - | if !m.previewing() || m.overviewAxis() != horizontalAxis { | |
| 166 | - | return -1 | |
| 167 | - | } | |
| 168 | - | return m.cardAt(w) | |
| 169 | - | } | |
| 170 | - | ||
| 171 | 161 | // overviewTarget resolves the active preview to one window for validation | |
| 172 | 162 | // and window actions. Presentation state never falls back to a covering float. | |
| 173 | 163 | func (m *Monitor) overviewTarget() WindowID { | |
| @@ -412,11 +402,11 @@ func (m *Monitor) OverviewMove(dx, dy int) { | |||
| 412 | 402 | } | |
| 413 | 403 | ||
| 414 | 404 | // overviewWorkspaceStep is a focus-workspace bind. A vertical overview | |
| 415 | - | // walks the unfolded stash and the stack cards before changing workspace | |
| 416 | - | // (overviewVertical). A side-by-side one always changes workspace, since | |
| 417 | - | // its stack and unfolded stash run left and right. | |
| 405 | + | // walks the stack cards before changing workspace (overviewVertical), not | |
| 406 | + | // the unfolded stash, which wraps round. A side-by-side one always changes | |
| 407 | + | // workspace, since its stack and unfolded stash run left and right. | |
| 418 | 408 | func (m *Monitor) overviewWorkspaceStep(d int) { | |
| 419 | - | if m.overviewAxis() == verticalAxis { | |
| 409 | + | if m.overviewAxis() == verticalAxis && m.cardAt(m.Current()) < 0 { | |
| 420 | 410 | m.overviewVertical(d) | |
| 421 | 411 | return | |
| 422 | 412 | } | |
| @@ -424,17 +414,45 @@ func (m *Monitor) overviewWorkspaceStep(d int) { | |||
| 424 | 414 | } | |
| 425 | 415 | ||
| 426 | 416 | // cycleCard selects the stash card d entries from the selected one of w in | |
| 427 | - | // the unfolded pile, stopping at either end. It reports whether a card was | |
| 428 | - | // selected. | |
| 417 | + | // the unfolded pile, wrapping round (wrapsCards), else stopping at either | |
| 418 | + | // end. It reports whether a card was selected. | |
| 429 | 419 | func (m *Monitor) cycleCard(w *Workspace, d int) bool { | |
| 430 | 420 | at := m.cardAt(w) | |
| 431 | 421 | if at < 0 { | |
| 432 | 422 | return false | |
| 433 | 423 | } | |
| 434 | - | m.selectCard(w, min(max(at+d, 0), len(w.Stash)-1)) | |
| 424 | + | if !m.stepCard(w, at, d) { | |
| 425 | + | m.selectCard(w, min(max(at+d, 0), len(w.Stash)-1)) | |
| 426 | + | } | |
| 427 | + | return true | |
| 428 | + | } | |
| 429 | + | ||
| 430 | + | // stepCard selects the stash card d entries from card at of w, wrapping | |
| 431 | + | // round when the carousel does (wrapsCards). It reports false, selecting | |
| 432 | + | // nothing, past an end of one that does not. | |
| 433 | + | func (m *Monitor) stepCard(w *Workspace, at, d int) bool { | |
| 434 | + | n, next := len(w.Stash), at+d | |
| 435 | + | if next < 0 || next >= n { | |
| 436 | + | if !w.wrapsCards() { | |
| 437 | + | return false | |
| 438 | + | } | |
| 439 | + | next = (next%n + n) % n | |
| 440 | + | } | |
| 441 | + | m.selectCard(w, next) | |
| 435 | 442 | return true | |
| 436 | 443 | } | |
| 437 | 444 | ||
| 445 | + | // wrapsCards reports whether the unfolded stash of w wraps round, drawn | |
| 446 | + | // (unfold) and walked (stepCard). A carousel of fewer than three cards does | |
| 447 | + | // not, so that each side has the card its move selects; nor does the one | |
| 448 | + | // of a stash-only workspace, which has nothing beside its pile to leave it | |
| 449 | + | // for. Past the ends of one that does not, a move goes on as from a lane | |
| 450 | + | // (cascade) or stops (a vertical overview). | |
| 451 | + | func (w *Workspace) wrapsCards() bool { | |
| 452 | + | _, k := w.stackHead() | |
| 453 | + | return k > 0 && len(w.Stash) >= 3 | |
| 454 | + | } | |
| 455 | + | ||
| 438 | 456 | // overviewLane is the vertical move of an overview whose workspaces sit side | |
| 439 | 457 | // by side (cascade). From the unfolded stash, up leaves it for the last lane | |
| 440 | 458 | // (leavePile) and down stays: the stash is the bottom. Else the selection | |
| @@ -490,29 +508,18 @@ func (m *Monitor) selectLaneColumn(w *Workspace, i int) { | |||
| 490 | 508 | ||
| 491 | 509 | // overviewAcross is the horizontal move of an overview whose workspaces sit | |
| 492 | 510 | // side by side (cascade): a column inside the selected lane, or a card of | |
| 493 | - | // the unfolded stash, which wraps round (carousel). Past the first column | |
| 494 | - | // it goes to the previous workspace; past the last, to the next workspace. | |
| 495 | - | // A carousel of fewer than three cards does not wrap, nor does the one of a | |
| 496 | - | // stash-only workspace, which has nothing above its pile: there the ends | |
| 497 | - | // change workspace like a lane's. | |
| 511 | + | // the unfolded stash, which wraps round (stepCard). Past the first column | |
| 512 | + | // or a card that does not wrap, it goes to the previous workspace; past | |
| 513 | + | // the last, to the next workspace. | |
| 498 | 514 | // Covering floats fan | |
| 499 | 515 | // along this axis and are traversed before a workspace: the card on the | |
| 500 | 516 | // left is the one behind. | |
| 501 | 517 | func (m *Monitor) overviewAcross(dx int) { | |
| 502 | 518 | w := m.Current() | |
| 503 | 519 | if at := m.cardAt(w); at >= 0 { | |
| 504 | - | n, next := len(w.Stash), at+dx | |
| 505 | - | if next >= 0 && next < n { | |
| 506 | - | m.selectCard(w, next) | |
| 507 | - | return | |
| 508 | - | } | |
| 509 | - | if _, k := w.stackHead(); k == 0 || n < 3 { | |
| 510 | - | // Nothing above the pile to leave it for, or too few | |
| 511 | - | // cards to wrap round: the ends are the way out. | |
| 520 | + | if !m.stepCard(w, at, dx) { | |
| 512 | 521 | m.overviewStep(dx) | |
| 513 | - | return | |
| 514 | 522 | } | |
| 515 | - | m.selectCard(w, (next+n)%n) | |
| 516 | 523 | return | |
| 517 | 524 | } | |
| 518 | 525 | if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 { | |
| @@ -937,86 +944,62 @@ func (w *Workspace) pile(dst []Placement, axis layoutAxis, slot Rect, dim bool, | |||
| 937 | 944 | return dst | |
| 938 | 945 | } | |
| 939 | 946 | ||
| 940 | - | // unfold lays the stash of w out from the folded pile's card rect. A | |
| 941 | - | // vertical overview lines every card up in a column (strip). A horizontal | |
| 942 | - | // one spreads them as a carousel (carousel). Cards other than the selected | |
| 943 | - | // one at are dimmed; those out of the overview area are hidden. The cards | |
| 944 | - | // are appended to dst. | |
| 947 | + | // unfold lays the stash of w out from the folded pile's card rect as a | |
| 948 | + | // carousel along axis: a row in a horizontal overview, a column in a | |
| 949 | + | // vertical one. The selected card at sits on the pile, in the middle; the | |
| 950 | + | // cards before it come on the left (above), the ones after it on the right | |
| 951 | + | // (below), in stash order, wrapping round (wrapsCards) with the oldest | |
| 952 | + | // following the newest (carouselSlot). A wider gap marks that seam. When the carousel fits the | |
| 953 | + | // overview area it is shifted into it; else the cards past the area are | |
| 954 | + | // hidden. Cards other than at are dimmed. The cards are appended to dst. | |
| 945 | 955 | func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int) []Placement { | |
| 946 | - | if axis == horizontalAxis { | |
| 947 | - | return w.carousel(dst, folded, at) | |
| 948 | - | } | |
| 949 | - | return w.strip(dst, folded, at) | |
| 950 | - | } | |
| 951 | - | ||
| 952 | - | // strip lays the stash out as a column of every card, in stash order, | |
| 953 | - | // centred on the pile and kept in the overview area. Longer than the area, | |
| 954 | - | // it scrolls like a wide row: the selected card at is centred, the strip | |
| 955 | - | // stopping at the area's edges. | |
| 956 | - | func (w *Workspace) strip(dst []Placement, folded Rect, at int) []Placement { | |
| 957 | 956 | n := len(w.Stash) | |
| 958 | 957 | u := w.overviewArea() | |
| 959 | - | size, lo, hi := folded.H, u.Y, u.Y+u.H | |
| 960 | - | stride := size + 2*w.cardStep() | |
| 961 | - | total := n*stride - (stride - size) | |
| 962 | - | start := folded.Y + size/2 - total/2 | |
| 963 | - | if total > hi-lo { | |
| 964 | - | start = folded.Y - at*stride | |
| 965 | - | start = min(max(start, hi-total), lo) | |
| 966 | - | } else { | |
| 967 | - | start = min(max(start, lo), hi-total) | |
| 968 | - | } | |
| 969 | - | for i := range w.Stash { | |
| 970 | - | r := folded | |
| 971 | - | r.Y = start + i*stride | |
| 972 | - | dst = w.unfoldedCard(dst, i, r, at, u) | |
| 958 | + | size, origin, lo, room := folded.W, folded.X, u.X, u.W | |
| 959 | + | if axis == verticalAxis { | |
| 960 | + | size, origin, lo, room = folded.H, folded.Y, u.Y, u.H | |
| 973 | 961 | } | |
| 974 | - | return dst | |
| 975 | - | } | |
| 976 | - | ||
| 977 | - | // carousel lays the stash out as a row centred on the selected card at, | |
| 978 | - | // under the pile, in stash order wrapping round: the cards before it on | |
| 979 | - | // the left, the ones after it on the right, the oldest following the | |
| 980 | - | // newest (carouselSlot). A wider gap marks that seam. When the row fits | |
| 981 | - | // the overview area it is shifted into it; else the cards past the area | |
| 982 | - | // are hidden. | |
| 983 | - | func (w *Workspace) carousel(dst []Placement, folded Rect, at int) []Placement { | |
| 984 | - | n := len(w.Stash) | |
| 985 | - | u := w.overviewArea() | |
| 986 | - | stride := folded.W + 2*w.cardStep() | |
| 962 | + | stride := size + 2*w.cardStep() | |
| 987 | 963 | // The extra room at the seam, on top of the stride. | |
| 988 | - | gap := folded.W / 4 | |
| 989 | - | x := func(i int) int { | |
| 990 | - | s := carouselSlot(i, at, n) | |
| 991 | - | x := folded.X + s*stride | |
| 964 | + | gap := size / 4 | |
| 965 | + | wraps := w.wrapsCards() | |
| 966 | + | pos := func(i int) int { | |
| 967 | + | s := carouselSlot(i, at, n, wraps) | |
| 968 | + | p := origin + s*stride | |
| 992 | 969 | switch { | |
| 993 | 970 | case s > 0 && at+s >= n: | |
| 994 | - | x += gap | |
| 971 | + | p += gap | |
| 995 | 972 | case s < 0 && at+s < 0: | |
| 996 | - | x -= gap | |
| 973 | + | p -= gap | |
| 997 | 974 | } | |
| 998 | - | return x | |
| 975 | + | return p | |
| 976 | + | } | |
| 977 | + | first, last := pos(0), pos(n-1) | |
| 978 | + | if wraps { | |
| 979 | + | first, last = pos((at+n-n/2)%n), pos((at+(n-1)/2)%n) | |
| 999 | 980 | } | |
| 1000 | - | first, last := x((at+n-n/2)%n), x((at+(n-1)/2)%n) | |
| 1001 | 981 | shift := 0 | |
| 1002 | - | if total := last + folded.W - first; total <= u.W { | |
| 1003 | - | shift = min(max(first, u.X), u.X+u.W-total) - first | |
| 982 | + | if total := last + size - first; total <= room { | |
| 983 | + | shift = min(max(first, lo), lo+room-total) - first | |
| 1004 | 984 | } | |
| 1005 | 985 | for i := range w.Stash { | |
| 1006 | 986 | r := folded | |
| 1007 | - | r.X = x(i) + shift | |
| 987 | + | if axis == verticalAxis { | |
| 988 | + | r.Y = pos(i) + shift | |
| 989 | + | } else { | |
| 990 | + | r.X = pos(i) + shift | |
| 991 | + | } | |
| 1008 | 992 | dst = w.unfoldedCard(dst, i, r, at, u) | |
| 1009 | 993 | } | |
| 1010 | 994 | return dst | |
| 1011 | 995 | } | |
| 1012 | 996 | ||
| 1013 | 997 | // carouselSlot is the place of stash card i in a carousel of n cards | |
| 1014 | - | // centred on card at: 0 for at, negative on the left, positive on the | |
| 1015 | - | // right. The left side takes the extra card of an even count. Fewer than | |
| 1016 | - | // three cards keep the stash order without wrapping round, so that each | |
| 1017 | - | // side has the card its move selects. | |
| 1018 | - | func carouselSlot(i, at, n int) int { | |
| 1019 | - | if n < 3 { | |
| 998 | + | // centred on card at: 0 for at, negative on the left (above), positive on | |
| 999 | + | // the right (below). Wrapping round, the left side takes the extra card of | |
| 1000 | + | // an even count; else the cards keep the stash order. | |
| 1001 | + | func carouselSlot(i, at, n int, wraps bool) int { | |
| 1002 | + | if !wraps { | |
| 1020 | 1003 | return i - at | |
| 1021 | 1004 | } | |
| 1022 | 1005 | s := (i - at + n) % n | |
| @@ -57,9 +57,8 @@ type rowSnapshot struct { | |||||
| 57 | // (not selected), the stash's own selection is in front at | 57 | // (not selected), the stash's own selection is in front at | ||
| 58 | // overviewCardZoom, up to overviewCards-1 others behind it, each | 58 | // overviewCardZoom, up to overviewCards-1 others behind it, each | ||
| 59 | // overviewCardStep further up and right, dimmed. Selected, the pile | 59 | // overviewCardStep further up and right, dimmed. Selected, the pile | ||
| 60 | - | // unfolds along the overview axis (unfold), the selection framed, and the | 60 | + | // unfolds along the overview axis into a carousel centred on the selection |
| 61 | - | // moves along that axis walk it: a column of every card in a vertical | 61 | + | // (unfold), framed, and the moves along that axis walk it round. A vertical |
| 62 | - | // overview, a carousel centred on the selection in a horizontal one. A vertical | |||
| 63 | // overview pins the pile to the left edge of its row, which takes the width | 62 | // overview pins the pile to the left edge of its row, which takes the width | ||
| 64 | // the pile leaves. A horizontal one centres it on its row, in a band | 63 | // the pile leaves. A horizontal one centres it on its row, in a band | ||
| 65 | // reserved under the selected lane (pileBand): the lane and the band are | 64 | // reserved under the selected lane (pileBand): the lane and the band are | ||
| @@ -159,15 +158,6 @@ func (m *Monitor) cardAt(w *Workspace) int { | |||||
| 159 | return -1 | 158 | return -1 | ||
| 160 | } | 159 | } | ||
| 161 | 160 | ||||
| 162 | - | // carouselCard is the selected card of w's stash when it unfolds as a | |||
| 163 | - | // carousel (a horizontal overview), -1 otherwise. | |||
| 164 | - | func (m *Monitor) carouselCard(w *Workspace) int { | |||
| 165 | - | if !m.previewing() || m.overviewAxis() != horizontalAxis { | |||
| 166 | - | return -1 | |||
| 167 | - | } | |||
| 168 | - | return m.cardAt(w) | |||
| 169 | - | } | |||
| 170 | - | ||||
| 171 | // overviewTarget resolves the active preview to one window for validation | 161 | // overviewTarget resolves the active preview to one window for validation | ||
| 172 | // and window actions. Presentation state never falls back to a covering float. | 162 | // and window actions. Presentation state never falls back to a covering float. | ||
| 173 | func (m *Monitor) overviewTarget() WindowID { | 163 | func (m *Monitor) overviewTarget() WindowID { | ||
| @@ -412,11 +402,11 @@ func (m *Monitor) OverviewMove(dx, dy int) { | |||||
| 412 | } | 402 | } | ||
| 413 | 403 | ||||
| 414 | // overviewWorkspaceStep is a focus-workspace bind. A vertical overview | 404 | // overviewWorkspaceStep is a focus-workspace bind. A vertical overview | ||
| 415 | - | // walks the unfolded stash and the stack cards before changing workspace | 405 | + | // walks the stack cards before changing workspace (overviewVertical), not |
| 416 | - | // (overviewVertical). A side-by-side one always changes workspace, since | 406 | + | // the unfolded stash, which wraps round. A side-by-side one always changes |
| 417 | - | // its stack and unfolded stash run left and right. | 407 | + | // workspace, since its stack and unfolded stash run left and right. |
| 418 | func (m *Monitor) overviewWorkspaceStep(d int) { | 408 | func (m *Monitor) overviewWorkspaceStep(d int) { | ||
| 419 | - | if m.overviewAxis() == verticalAxis { | 409 | + | if m.overviewAxis() == verticalAxis && m.cardAt(m.Current()) < 0 { |
| 420 | m.overviewVertical(d) | 410 | m.overviewVertical(d) | ||
| 421 | return | 411 | return | ||
| 422 | } | 412 | } | ||
| @@ -424,17 +414,45 @@ func (m *Monitor) overviewWorkspaceStep(d int) { | |||||
| 424 | } | 414 | } | ||
| 425 | 415 | ||||
| 426 | // cycleCard selects the stash card d entries from the selected one of w in | 416 | // cycleCard selects the stash card d entries from the selected one of w in | ||
| 427 | - | // the unfolded pile, stopping at either end. It reports whether a card was | 417 | + | // the unfolded pile, wrapping round (wrapsCards), else stopping at either |
| 428 | - | // selected. | 418 | + | // end. It reports whether a card was selected. |
| 429 | func (m *Monitor) cycleCard(w *Workspace, d int) bool { | 419 | func (m *Monitor) cycleCard(w *Workspace, d int) bool { | ||
| 430 | at := m.cardAt(w) | 420 | at := m.cardAt(w) | ||
| 431 | if at < 0 { | 421 | if at < 0 { | ||
| 432 | return false | 422 | return false | ||
| 433 | } | 423 | } | ||
| 434 | - | m.selectCard(w, min(max(at+d, 0), len(w.Stash)-1)) | 424 | + | if !m.stepCard(w, at, d) { |
| 425 | + | m.selectCard(w, min(max(at+d, 0), len(w.Stash)-1)) | |||
| 426 | + | } | |||
| 427 | + | return true | |||
| 428 | + | } | |||
| 429 | + | ||||
| 430 | + | // stepCard selects the stash card d entries from card at of w, wrapping | |||
| 431 | + | // round when the carousel does (wrapsCards). It reports false, selecting | |||
| 432 | + | // nothing, past an end of one that does not. | |||
| 433 | + | func (m *Monitor) stepCard(w *Workspace, at, d int) bool { | |||
| 434 | + | n, next := len(w.Stash), at+d | |||
| 435 | + | if next < 0 || next >= n { | |||
| 436 | + | if !w.wrapsCards() { | |||
| 437 | + | return false | |||
| 438 | + | } | |||
| 439 | + | next = (next%n + n) % n | |||
| 440 | + | } | |||
| 441 | + | m.selectCard(w, next) | |||
| 435 | return true | 442 | return true | ||
| 436 | } | 443 | } | ||
| 437 | 444 | ||||
| 445 | + | // wrapsCards reports whether the unfolded stash of w wraps round, drawn | |||
| 446 | + | // (unfold) and walked (stepCard). A carousel of fewer than three cards does | |||
| 447 | + | // not, so that each side has the card its move selects; nor does the one | |||
| 448 | + | // of a stash-only workspace, which has nothing beside its pile to leave it | |||
| 449 | + | // for. Past the ends of one that does not, a move goes on as from a lane | |||
| 450 | + | // (cascade) or stops (a vertical overview). | |||
| 451 | + | func (w *Workspace) wrapsCards() bool { | |||
| 452 | + | _, k := w.stackHead() | |||
| 453 | + | return k > 0 && len(w.Stash) >= 3 | |||
| 454 | + | } | |||
| 455 | + | ||||
| 438 | // overviewLane is the vertical move of an overview whose workspaces sit side | 456 | // overviewLane is the vertical move of an overview whose workspaces sit side | ||
| 439 | // by side (cascade). From the unfolded stash, up leaves it for the last lane | 457 | // by side (cascade). From the unfolded stash, up leaves it for the last lane | ||
| 440 | // (leavePile) and down stays: the stash is the bottom. Else the selection | 458 | // (leavePile) and down stays: the stash is the bottom. Else the selection | ||
| @@ -490,29 +508,18 @@ func (m *Monitor) selectLaneColumn(w *Workspace, i int) { | |||||
| 490 | 508 | ||||
| 491 | // overviewAcross is the horizontal move of an overview whose workspaces sit | 509 | // overviewAcross is the horizontal move of an overview whose workspaces sit | ||
| 492 | // side by side (cascade): a column inside the selected lane, or a card of | 510 | // side by side (cascade): a column inside the selected lane, or a card of | ||
| 493 | - | // the unfolded stash, which wraps round (carousel). Past the first column | 511 | + | // the unfolded stash, which wraps round (stepCard). Past the first column |
| 494 | - | // it goes to the previous workspace; past the last, to the next workspace. | 512 | + | // or a card that does not wrap, it goes to the previous workspace; past |
| 495 | - | // A carousel of fewer than three cards does not wrap, nor does the one of a | 513 | + | // the last, to the next workspace. |
| 496 | - | // stash-only workspace, which has nothing above its pile: there the ends | |||
| 497 | - | // change workspace like a lane's. | |||
| 498 | // Covering floats fan | 514 | // Covering floats fan | ||
| 499 | // along this axis and are traversed before a workspace: the card on the | 515 | // along this axis and are traversed before a workspace: the card on the | ||
| 500 | // left is the one behind. | 516 | // left is the one behind. | ||
| 501 | func (m *Monitor) overviewAcross(dx int) { | 517 | func (m *Monitor) overviewAcross(dx int) { | ||
| 502 | w := m.Current() | 518 | w := m.Current() | ||
| 503 | if at := m.cardAt(w); at >= 0 { | 519 | if at := m.cardAt(w); at >= 0 { | ||
| 504 | - | n, next := len(w.Stash), at+dx | 520 | + | if !m.stepCard(w, at, dx) { |
| 505 | - | if next >= 0 && next < n { | |||
| 506 | - | m.selectCard(w, next) | |||
| 507 | - | return | |||
| 508 | - | } | |||
| 509 | - | if _, k := w.stackHead(); k == 0 || n < 3 { | |||
| 510 | - | // Nothing above the pile to leave it for, or too few | |||
| 511 | - | // cards to wrap round: the ends are the way out. | |||
| 512 | m.overviewStep(dx) | 521 | m.overviewStep(dx) | ||
| 513 | - | return | |||
| 514 | } | 522 | } | ||
| 515 | - | m.selectCard(w, (next+n)%n) | |||
| 516 | return | 523 | return | ||
| 517 | } | 524 | } | ||
| 518 | if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 { | 525 | if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 { | ||
| @@ -937,86 +944,62 @@ func (w *Workspace) pile(dst []Placement, axis layoutAxis, slot Rect, dim bool, | |||||
| 937 | return dst | 944 | return dst | ||
| 938 | } | 945 | } | ||
| 939 | 946 | ||||
| 940 | - | // unfold lays the stash of w out from the folded pile's card rect. A | 947 | + | // unfold lays the stash of w out from the folded pile's card rect as a |
| 941 | - | // vertical overview lines every card up in a column (strip). A horizontal | 948 | + | // carousel along axis: a row in a horizontal overview, a column in a |
| 942 | - | // one spreads them as a carousel (carousel). Cards other than the selected | 949 | + | // vertical one. The selected card at sits on the pile, in the middle; the |
| 943 | - | // one at are dimmed; those out of the overview area are hidden. The cards | 950 | + | // cards before it come on the left (above), the ones after it on the right |
| 944 | - | // are appended to dst. | 951 | + | // (below), in stash order, wrapping round (wrapsCards) with the oldest |
| 952 | + | // following the newest (carouselSlot). A wider gap marks that seam. When the carousel fits the | |||
| 953 | + | // overview area it is shifted into it; else the cards past the area are | |||
| 954 | + | // hidden. Cards other than at are dimmed. The cards are appended to dst. | |||
| 945 | func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int) []Placement { | 955 | func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int) []Placement { | ||
| 946 | - | if axis == horizontalAxis { | |||
| 947 | - | return w.carousel(dst, folded, at) | |||
| 948 | - | } | |||
| 949 | - | return w.strip(dst, folded, at) | |||
| 950 | - | } | |||
| 951 | - | ||||
| 952 | - | // strip lays the stash out as a column of every card, in stash order, | |||
| 953 | - | // centred on the pile and kept in the overview area. Longer than the area, | |||
| 954 | - | // it scrolls like a wide row: the selected card at is centred, the strip | |||
| 955 | - | // stopping at the area's edges. | |||
| 956 | - | func (w *Workspace) strip(dst []Placement, folded Rect, at int) []Placement { | |||
| 957 | n := len(w.Stash) | 956 | n := len(w.Stash) | ||
| 958 | u := w.overviewArea() | 957 | u := w.overviewArea() | ||
| 959 | - | size, lo, hi := folded.H, u.Y, u.Y+u.H | 958 | + | size, origin, lo, room := folded.W, folded.X, u.X, u.W |
| 960 | - | stride := size + 2*w.cardStep() | 959 | + | if axis == verticalAxis { |
| 961 | - | total := n*stride - (stride - size) | 960 | + | size, origin, lo, room = folded.H, folded.Y, u.Y, u.H |
| 962 | - | start := folded.Y + size/2 - total/2 | |||
| 963 | - | if total > hi-lo { | |||
| 964 | - | start = folded.Y - at*stride | |||
| 965 | - | start = min(max(start, hi-total), lo) | |||
| 966 | - | } else { | |||
| 967 | - | start = min(max(start, lo), hi-total) | |||
| 968 | - | } | |||
| 969 | - | for i := range w.Stash { | |||
| 970 | - | r := folded | |||
| 971 | - | r.Y = start + i*stride | |||
| 972 | - | dst = w.unfoldedCard(dst, i, r, at, u) | |||
| 973 | } | 961 | } | ||
| 974 | - | return dst | 962 | + | stride := size + 2*w.cardStep() |
| 975 | - | } | |||
| 976 | - | ||||
| 977 | - | // carousel lays the stash out as a row centred on the selected card at, | |||
| 978 | - | // under the pile, in stash order wrapping round: the cards before it on | |||
| 979 | - | // the left, the ones after it on the right, the oldest following the | |||
| 980 | - | // newest (carouselSlot). A wider gap marks that seam. When the row fits | |||
| 981 | - | // the overview area it is shifted into it; else the cards past the area | |||
| 982 | - | // are hidden. | |||
| 983 | - | func (w *Workspace) carousel(dst []Placement, folded Rect, at int) []Placement { | |||
| 984 | - | n := len(w.Stash) | |||
| 985 | - | u := w.overviewArea() | |||
| 986 | - | stride := folded.W + 2*w.cardStep() | |||
| 987 | // The extra room at the seam, on top of the stride. | 963 | // The extra room at the seam, on top of the stride. | ||
| 988 | - | gap := folded.W / 4 | 964 | + | gap := size / 4 |
| 989 | - | x := func(i int) int { | 965 | + | wraps := w.wrapsCards() |
| 990 | - | s := carouselSlot(i, at, n) | 966 | + | pos := func(i int) int { |
| 991 | - | x := folded.X + s*stride | 967 | + | s := carouselSlot(i, at, n, wraps) |
| 968 | + | p := origin + s*stride | |||
| 992 | switch { | 969 | switch { | ||
| 993 | case s > 0 && at+s >= n: | 970 | case s > 0 && at+s >= n: | ||
| 994 | - | x += gap | 971 | + | p += gap |
| 995 | case s < 0 && at+s < 0: | 972 | case s < 0 && at+s < 0: | ||
| 996 | - | x -= gap | 973 | + | p -= gap |
| 997 | } | 974 | } | ||
| 998 | - | return x | 975 | + | return p |
| 976 | + | } | |||
| 977 | + | first, last := pos(0), pos(n-1) | |||
| 978 | + | if wraps { | |||
| 979 | + | first, last = pos((at+n-n/2)%n), pos((at+(n-1)/2)%n) | |||
| 999 | } | 980 | } | ||
| 1000 | - | first, last := x((at+n-n/2)%n), x((at+(n-1)/2)%n) | |||
| 1001 | shift := 0 | 981 | shift := 0 | ||
| 1002 | - | if total := last + folded.W - first; total <= u.W { | 982 | + | if total := last + size - first; total <= room { |
| 1003 | - | shift = min(max(first, u.X), u.X+u.W-total) - first | 983 | + | shift = min(max(first, lo), lo+room-total) - first |
| 1004 | } | 984 | } | ||
| 1005 | for i := range w.Stash { | 985 | for i := range w.Stash { | ||
| 1006 | r := folded | 986 | r := folded | ||
| 1007 | - | r.X = x(i) + shift | 987 | + | if axis == verticalAxis { |
| 988 | + | r.Y = pos(i) + shift | |||
| 989 | + | } else { | |||
| 990 | + | r.X = pos(i) + shift | |||
| 991 | + | } | |||
| 1008 | dst = w.unfoldedCard(dst, i, r, at, u) | 992 | dst = w.unfoldedCard(dst, i, r, at, u) | ||
| 1009 | } | 993 | } | ||
| 1010 | return dst | 994 | return dst | ||
| 1011 | } | 995 | } | ||
| 1012 | 996 | ||||
| 1013 | // carouselSlot is the place of stash card i in a carousel of n cards | 997 | // carouselSlot is the place of stash card i in a carousel of n cards | ||
| 1014 | - | // centred on card at: 0 for at, negative on the left, positive on the | 998 | + | // centred on card at: 0 for at, negative on the left (above), positive on |
| 1015 | - | // right. The left side takes the extra card of an even count. Fewer than | 999 | + | // the right (below). Wrapping round, the left side takes the extra card of |
| 1016 | - | // three cards keep the stash order without wrapping round, so that each | 1000 | + | // an even count; else the cards keep the stash order. |
| 1017 | - | // side has the card its move selects. | 1001 | + | func carouselSlot(i, at, n int, wraps bool) int { |
| 1018 | - | func carouselSlot(i, at, n int) int { | 1002 | + | if !wraps { |
| 1019 | - | if n < 3 { | |||
| 1020 | return i - at | 1003 | return i - at | ||
| 1021 | } | 1004 | } | ||
| 1022 | s := (i - at + n) % n | 1005 | s := (i - at + n) % n | ||
M internal/core/overview_stash_input_test.go +24 -4
| @@ -43,9 +43,9 @@ func TestOverviewStashInputNavigation(t *testing.T) { | |||
| 43 | 43 | var a Action | |
| 44 | 44 | switch { | |
| 45 | 45 | case dx == 0 && dy < 0: | |
| 46 | - | a = ActionFocusWorkspacePrev | |
| 46 | + | a = ActionFocusWindowUp | |
| 47 | 47 | case dx == 0 && dy > 0: | |
| 48 | - | a = ActionFocusWorkspaceNext | |
| 48 | + | a = ActionFocusWindowDown | |
| 49 | 49 | case dx > 0 && dy == 0: | |
| 50 | 50 | a = ActionFocusColumnRight | |
| 51 | 51 | case dx < 0 && dy == 0: | |
| @@ -62,8 +62,8 @@ func TestOverviewStashInputNavigation(t *testing.T) { | |||
| 62 | 62 | } | |
| 63 | 63 | move(0, 1) | |
| 64 | 64 | move(0, 1) | |
| 65 | - | if m.Current() != w || m.cardAt(w) != 2 || !slices.Equal(stashIDs(w), before) { | |
| 66 | - | t.Fatal("down escaped or reordered the stash") | |
| 65 | + | if m.Current() != w || m.cardAt(w) != 0 || !slices.Equal(stashIDs(w), before) { | |
| 66 | + | t.Fatalf("down past the last card: card %d, want it to wrap round to 0 in order", m.cardAt(w)) | |
| 67 | 67 | } | |
| 68 | 68 | move(1, 0) | |
| 69 | 69 | if m.cardAt(w) >= 0 { | |
| @@ -72,3 +72,23 @@ func TestOverviewStashInputNavigation(t *testing.T) { | |||
| 72 | 72 | }) | |
| 73 | 73 | } | |
| 74 | 74 | } | |
| 75 | + | ||
| 76 | + | // In a vertical overview, a focus-workspace bind over the unfolded stash | |
| 77 | + | // changes workspace: the carousel wraps round, so walking it would never | |
| 78 | + | // get there. | |
| 79 | + | func TestOverviewWorkspaceBindLeavesCarousel(t *testing.T) { | |
| 80 | + | m := pileMonitor() | |
| 81 | + | w := m.Current() | |
| 82 | + | m.Focus(1) | |
| 83 | + | m.AddWindow(30) | |
| 84 | + | m.Focus(0) | |
| 85 | + | m.ToggleOverview() | |
| 86 | + | m.OverviewMove(-1, 0) | |
| 87 | + | if m.overviewAxis() != verticalAxis || m.cardAt(w) < 0 { | |
| 88 | + | t.Fatal("setup: no card selected in a vertical overview") | |
| 89 | + | } | |
| 90 | + | m.overviewFocus(ActionFocusWorkspaceNext) | |
| 91 | + | if m.Current() == w { | |
| 92 | + | t.Fatal("focus-workspace-next over a card stayed on the workspace") | |
| 93 | + | } | |
| 94 | + | } | |
| @@ -43,9 +43,9 @@ func TestOverviewStashInputNavigation(t *testing.T) { | |||||
| 43 | var a Action | 43 | var a Action | ||
| 44 | switch { | 44 | switch { | ||
| 45 | case dx == 0 && dy < 0: | 45 | case dx == 0 && dy < 0: | ||
| 46 | - | a = ActionFocusWorkspacePrev | 46 | + | a = ActionFocusWindowUp |
| 47 | case dx == 0 && dy > 0: | 47 | case dx == 0 && dy > 0: | ||
| 48 | - | a = ActionFocusWorkspaceNext | 48 | + | a = ActionFocusWindowDown |
| 49 | case dx > 0 && dy == 0: | 49 | case dx > 0 && dy == 0: | ||
| 50 | a = ActionFocusColumnRight | 50 | a = ActionFocusColumnRight | ||
| 51 | case dx < 0 && dy == 0: | 51 | case dx < 0 && dy == 0: | ||
| @@ -62,8 +62,8 @@ func TestOverviewStashInputNavigation(t *testing.T) { | |||||
| 62 | } | 62 | } | ||
| 63 | move(0, 1) | 63 | move(0, 1) | ||
| 64 | move(0, 1) | 64 | move(0, 1) | ||
| 65 | - | if m.Current() != w || m.cardAt(w) != 2 || !slices.Equal(stashIDs(w), before) { | 65 | + | if m.Current() != w || m.cardAt(w) != 0 || !slices.Equal(stashIDs(w), before) { |
| 66 | - | t.Fatal("down escaped or reordered the stash") | 66 | + | t.Fatalf("down past the last card: card %d, want it to wrap round to 0 in order", m.cardAt(w)) |
| 67 | } | 67 | } | ||
| 68 | move(1, 0) | 68 | move(1, 0) | ||
| 69 | if m.cardAt(w) >= 0 { | 69 | if m.cardAt(w) >= 0 { | ||
| @@ -72,3 +72,23 @@ func TestOverviewStashInputNavigation(t *testing.T) { | |||||
| 72 | }) | 72 | }) | ||
| 73 | } | 73 | } | ||
| 74 | } | 74 | } | ||
| 75 | + | ||||
| 76 | + | // In a vertical overview, a focus-workspace bind over the unfolded stash | |||
| 77 | + | // changes workspace: the carousel wraps round, so walking it would never | |||
| 78 | + | // get there. | |||
| 79 | + | func TestOverviewWorkspaceBindLeavesCarousel(t *testing.T) { | |||
| 80 | + | m := pileMonitor() | |||
| 81 | + | w := m.Current() | |||
| 82 | + | m.Focus(1) | |||
| 83 | + | m.AddWindow(30) | |||
| 84 | + | m.Focus(0) | |||
| 85 | + | m.ToggleOverview() | |||
| 86 | + | m.OverviewMove(-1, 0) | |||
| 87 | + | if m.overviewAxis() != verticalAxis || m.cardAt(w) < 0 { | |||
| 88 | + | t.Fatal("setup: no card selected in a vertical overview") | |||
| 89 | + | } | |||
| 90 | + | m.overviewFocus(ActionFocusWorkspaceNext) | |||
| 91 | + | if m.Current() == w { | |||
| 92 | + | t.Fatal("focus-workspace-next over a card stayed on the workspace") | |||
| 93 | + | } | |||
| 94 | + | } | |||
M internal/core/overview_test.go +8 -10
| @@ -355,7 +355,7 @@ func TestOverviewPileKeepsRowCentred(t *testing.T) { | |||
| 355 | 355 | } | |
| 356 | 356 | } | |
| 357 | 357 | ||
| 358 | - | // Left enters the stash pile, up/down browse it without changing rows, | |
| 358 | + | // Left enters the stash pile, up/down browse it round without changing rows, | |
| 359 | 359 | // and right leaves it immediately. Confirm shows the chosen stash window. | |
| 360 | 360 | func TestOverviewPileNavigation(t *testing.T) { | |
| 361 | 361 | m := pileMonitor() | |
| @@ -375,17 +375,15 @@ func TestOverviewPileNavigation(t *testing.T) { | |||
| 375 | 375 | if at := m.cardAt(w); at != 2 { | |
| 376 | 376 | t.Fatalf("left changed stash selection to %d", at) | |
| 377 | 377 | } | |
| 378 | - | m.OverviewMove(0, 1) // stops at the last entry, not the next workspace | |
| 379 | - | if m.Current() != w || m.cardAt(w) != 2 { | |
| 380 | - | t.Fatal("down escaped the stash pile") | |
| 378 | + | m.OverviewMove(0, 1) // wraps round to the first entry, not the next workspace | |
| 379 | + | if m.Current() != w || m.cardAt(w) != 0 { | |
| 380 | + | t.Fatalf("down past the last entry: card %d, workspace changed %v", m.cardAt(w), m.Current() != w) | |
| 381 | 381 | } | |
| 382 | - | m.OverviewMove(0, -1) | |
| 383 | - | m.OverviewMove(0, -1) | |
| 384 | - | m.OverviewMove(0, -1) // stops at the first entry | |
| 385 | - | if at := m.cardAt(w); at != 0 || m.Current() != w { | |
| 386 | - | t.Fatalf("up end %d, workspace changed %v", at, m.Current() != w) | |
| 382 | + | m.OverviewMove(0, -1) // and back round to the last | |
| 383 | + | if at := m.cardAt(w); at != 2 || m.Current() != w { | |
| 384 | + | t.Fatalf("up past the first entry: card %d, workspace changed %v", at, m.Current() != w) | |
| 387 | 385 | } | |
| 388 | - | m.OverviewMove(0, 1) | |
| 386 | + | m.OverviewMove(0, -1) | |
| 389 | 387 | m.ToggleOverview() | |
| 390 | 388 | if f, _ := m.Focused(); f != 6 || w.stashHidden || m.ov.open { | |
| 391 | 389 | t.Fatalf("confirm on %d, hidden %v", f, w.stashHidden) | |
| @@ -355,7 +355,7 @@ func TestOverviewPileKeepsRowCentred(t *testing.T) { | |||||
| 355 | } | 355 | } | ||
| 356 | } | 356 | } | ||
| 357 | 357 | ||||
| 358 | - | // Left enters the stash pile, up/down browse it without changing rows, | 358 | + | // Left enters the stash pile, up/down browse it round without changing rows, |
| 359 | // and right leaves it immediately. Confirm shows the chosen stash window. | 359 | // and right leaves it immediately. Confirm shows the chosen stash window. | ||
| 360 | func TestOverviewPileNavigation(t *testing.T) { | 360 | func TestOverviewPileNavigation(t *testing.T) { | ||
| 361 | m := pileMonitor() | 361 | m := pileMonitor() | ||
| @@ -375,17 +375,15 @@ func TestOverviewPileNavigation(t *testing.T) { | |||||
| 375 | if at := m.cardAt(w); at != 2 { | 375 | if at := m.cardAt(w); at != 2 { | ||
| 376 | t.Fatalf("left changed stash selection to %d", at) | 376 | t.Fatalf("left changed stash selection to %d", at) | ||
| 377 | } | 377 | } | ||
| 378 | - | m.OverviewMove(0, 1) // stops at the last entry, not the next workspace | 378 | + | m.OverviewMove(0, 1) // wraps round to the first entry, not the next workspace |
| 379 | - | if m.Current() != w || m.cardAt(w) != 2 { | 379 | + | if m.Current() != w || m.cardAt(w) != 0 { |
| 380 | - | t.Fatal("down escaped the stash pile") | 380 | + | t.Fatalf("down past the last entry: card %d, workspace changed %v", m.cardAt(w), m.Current() != w) |
| 381 | } | 381 | } | ||
| 382 | - | m.OverviewMove(0, -1) | 382 | + | m.OverviewMove(0, -1) // and back round to the last |
| 383 | - | m.OverviewMove(0, -1) | 383 | + | if at := m.cardAt(w); at != 2 || m.Current() != w { |
| 384 | - | m.OverviewMove(0, -1) // stops at the first entry | 384 | + | t.Fatalf("up past the first entry: card %d, workspace changed %v", at, m.Current() != w) |
| 385 | - | if at := m.cardAt(w); at != 0 || m.Current() != w { | |||
| 386 | - | t.Fatalf("up end %d, workspace changed %v", at, m.Current() != w) | |||
| 387 | } | 385 | } | ||
| 388 | - | m.OverviewMove(0, 1) | 386 | + | m.OverviewMove(0, -1) |
| 389 | m.ToggleOverview() | 387 | m.ToggleOverview() | ||
| 390 | if f, _ := m.Focused(); f != 6 || w.stashHidden || m.ov.open { | 388 | if f, _ := m.Focused(); f != 6 || w.stashHidden || m.ov.open { | ||
| 391 | t.Fatalf("confirm on %d, hidden %v", f, w.stashHidden) | 389 | t.Fatalf("confirm on %d, hidden %v", f, w.stashHidden) | ||
M internal/core/overview_unfold_test.go +126 -91
| @@ -144,86 +144,58 @@ func TestOverviewCascadeStashFolds(t *testing.T) { | |||
| 144 | 144 | } | |
| 145 | 145 | } | |
| 146 | 146 | ||
| 147 | - | // A vertical overview's stash longer than the area scrolls like a wide | |
| 148 | - | // row: every card is reached in order, the selection stays inside the area | |
| 149 | - | // and is centred unless the strip stops at an edge of it, and cards past | |
| 150 | - | // the area are hidden. | |
| 151 | - | func TestOverviewStashUnfoldScrolls(t *testing.T) { | |
| 152 | - | m := cascadeOverview() | |
| 153 | - | w := m.Current() | |
| 154 | - | w.Overflow = OverflowScroll | |
| 155 | - | withStash(m, 12) | |
| 156 | - | m.ToggleOverview() | |
| 157 | - | if m.overviewAxis() != verticalAxis { | |
| 158 | - | t.Fatal("setup: wrong overview axis") | |
| 159 | - | } | |
| 160 | - | m.selectCard(w, 0) | |
| 161 | - | u := w.overviewArea() | |
| 162 | - | lo, hi := u.Y, u.Y+u.H | |
| 163 | - | centred, hidden := false, false | |
| 164 | - | for i := range len(w.Stash) { | |
| 165 | - | if m.cardAt(w) != i { | |
| 166 | - | t.Fatalf("step %d: card %d", i, m.cardAt(w)) | |
| 167 | - | } | |
| 168 | - | ps := m.Layout() | |
| 169 | - | sel := previewOf(t, ps, w.Stash[i].ID) | |
| 170 | - | at, size := sel.Rect.Y, sel.Rect.H | |
| 171 | - | if sel.Hidden || !sel.Focused || at < lo || at+size > hi { | |
| 172 | - | t.Fatalf("card %d off the area: %+v in %v", i, sel, u) | |
| 173 | - | } | |
| 174 | - | first := previewOf(t, ps, w.Stash[0].ID).Rect.Y | |
| 175 | - | last := previewOf(t, ps, w.Stash[len(w.Stash)-1].ID).Rect.Y | |
| 176 | - | if first > lo || last+size < hi { | |
| 177 | - | t.Fatalf("card %d: strip %d..%d does not fill the area %d..%d", i, first, last+size, lo, hi) | |
| 178 | - | } | |
| 179 | - | if mid := at + size/2; abs(mid-(lo+hi)/2) <= 1 { | |
| 180 | - | centred = true | |
| 181 | - | } else if first != lo && last+size != hi { | |
| 182 | - | t.Fatalf("card %d at %d, neither centred on %d nor clamped", i, mid, (lo+hi)/2) | |
| 183 | - | } | |
| 184 | - | for _, e := range w.Stash { | |
| 185 | - | hidden = hidden || previewOf(t, ps, e.ID).Hidden | |
| 186 | - | } | |
| 187 | - | m.OverviewMove(0, 1) | |
| 188 | - | } | |
| 189 | - | if !hidden || !centred { | |
| 190 | - | t.Fatalf("12 cards: hidden %v, centred %v; the test does not scroll", hidden, centred) | |
| 191 | - | } | |
| 192 | - | } | |
| 193 | - | ||
| 194 | - | // A carousel wider than the area keeps every selection centred in it, with | |
| 195 | - | // cards on both sides; the cards past the area are hidden. | |
| 196 | - | func TestOverviewCascadeCarouselScrolls(t *testing.T) { | |
| 197 | - | m := cascadeOverview() | |
| 198 | - | w := m.Current() | |
| 199 | - | withStash(m, 12) | |
| 200 | - | m.ToggleOverview() | |
| 201 | - | if m.overviewAxis() != horizontalAxis { | |
| 202 | - | t.Fatal("setup: wrong overview axis") | |
| 203 | - | } | |
| 204 | - | m.selectCard(w, 11) | |
| 205 | - | u := w.overviewArea() | |
| 206 | - | for step := range len(w.Stash) + 1 { | |
| 207 | - | i := m.cardAt(w) | |
| 208 | - | ps := m.Layout() | |
| 209 | - | sel := previewOf(t, ps, w.Stash[i].ID) | |
| 210 | - | if sel.Hidden || !sel.Focused || abs(sel.Rect.X+sel.Rect.W/2-(u.X+u.W/2)) > 1 { | |
| 211 | - | t.Fatalf("step %d: card %d not centred: %+v in %v", step, i, sel, u) | |
| 212 | - | } | |
| 213 | - | left, right, hidden := false, false, false | |
| 214 | - | for _, e := range w.Stash { | |
| 215 | - | p := previewOf(t, ps, e.ID) | |
| 216 | - | if p.Hidden { | |
| 217 | - | hidden = true | |
| 218 | - | continue | |
| 147 | + | // A carousel longer than the area keeps every selection centred in it, | |
| 148 | + | // with cards on both sides, on either axis; the cards past the area are | |
| 149 | + | // hidden. | |
| 150 | + | func TestOverviewCarouselScrolls(t *testing.T) { | |
| 151 | + | for _, axis := range []layoutAxis{horizontalAxis, verticalAxis} { | |
| 152 | + | t.Run(map[layoutAxis]string{horizontalAxis: "cascade", verticalAxis: "scroll"}[axis], func(t *testing.T) { | |
| 153 | + | m := cascadeOverview() | |
| 154 | + | w := m.Current() | |
| 155 | + | if axis == verticalAxis { | |
| 156 | + | w.Overflow = OverflowScroll | |
| 219 | 157 | } | |
| 220 | - | left = left || p.Rect.X < sel.Rect.X | |
| 221 | - | right = right || p.Rect.X > sel.Rect.X | |
| 222 | - | } | |
| 223 | - | if !left || !right || !hidden { | |
| 224 | - | t.Fatalf("step %d: cards left %v, right %v, hidden %v", step, left, right, hidden) | |
| 225 | - | } | |
| 226 | - | m.OverviewMove(1, 0) | |
| 158 | + | withStash(m, 12) | |
| 159 | + | m.ToggleOverview() | |
| 160 | + | if m.overviewAxis() != axis { | |
| 161 | + | t.Fatal("setup: wrong overview axis") | |
| 162 | + | } | |
| 163 | + | m.selectCard(w, 11) | |
| 164 | + | u := w.overviewArea() | |
| 165 | + | pos := func(r Rect) (int, int) { | |
| 166 | + | if axis == verticalAxis { | |
| 167 | + | return r.Y, r.Y + r.H/2 - (u.Y + u.H/2) | |
| 168 | + | } | |
| 169 | + | return r.X, r.X + r.W/2 - (u.X + u.W/2) | |
| 170 | + | } | |
| 171 | + | for step := range len(w.Stash) + 1 { | |
| 172 | + | i := m.cardAt(w) | |
| 173 | + | ps := m.Layout() | |
| 174 | + | sel := previewOf(t, ps, w.Stash[i].ID) | |
| 175 | + | at, off := pos(sel.Rect) | |
| 176 | + | if sel.Hidden || !sel.Focused || abs(off) > 1 { | |
| 177 | + | t.Fatalf("step %d: card %d not centred: %+v in %v", step, i, sel, u) | |
| 178 | + | } | |
| 179 | + | before, after, hidden := false, false, false | |
| 180 | + | for _, e := range w.Stash { | |
| 181 | + | p := previewOf(t, ps, e.ID) | |
| 182 | + | if p.Hidden { | |
| 183 | + | hidden = true | |
| 184 | + | continue | |
| 185 | + | } | |
| 186 | + | q, _ := pos(p.Rect) | |
| 187 | + | before, after = before || q < at, after || q > at | |
| 188 | + | } | |
| 189 | + | if !before || !after || !hidden { | |
| 190 | + | t.Fatalf("step %d: cards before %v, after %v, hidden %v", step, before, after, hidden) | |
| 191 | + | } | |
| 192 | + | if axis == verticalAxis { | |
| 193 | + | m.OverviewMove(0, 1) | |
| 194 | + | } else { | |
| 195 | + | m.OverviewMove(1, 0) | |
| 196 | + | } | |
| 197 | + | } | |
| 198 | + | }) | |
| 227 | 199 | } | |
| 228 | 200 | } | |
| 229 | 201 | ||
| @@ -251,8 +223,10 @@ func TestOverviewStashLayoutAllocations(t *testing.T) { | |||
| 251 | 223 | } | |
| 252 | 224 | } | |
| 253 | 225 | ||
| 254 | - | // A scroll overview unfolds its pile into a column on the left of the row; | |
| 255 | - | // up and down walk it, right goes back to the columns. | |
| 226 | + | // A scroll overview unfolds its pile into a carousel on the left of the | |
| 227 | + | // row: a column centred on the selection, the card before it above and the | |
| 228 | + | // one after it below, wrapping round. Up and down walk it round, right goes | |
| 229 | + | // back to the columns. | |
| 256 | 230 | func TestOverviewScrollStashUnfolds(t *testing.T) { | |
| 257 | 231 | m := overviewMonitor() | |
| 258 | 232 | w := m.Current() | |
| @@ -267,29 +241,90 @@ func TestOverviewScrollStashUnfolds(t *testing.T) { | |||
| 267 | 241 | t.Fatal("setup: not a vertical overview") | |
| 268 | 242 | } | |
| 269 | 243 | m.OverviewMove(-1, 0) | |
| 270 | - | if m.cardAt(w) < 0 { | |
| 244 | + | at := m.cardAt(w) | |
| 245 | + | if at < 0 { | |
| 271 | 246 | t.Fatal("left from the first column did not select the stash") | |
| 272 | 247 | } | |
| 273 | 248 | cards := shownCards(t, w, m.Layout()) | |
| 274 | 249 | if len(cards) != 3 { | |
| 275 | 250 | t.Fatalf("%d cards shown, want 3", len(cards)) | |
| 276 | 251 | } | |
| 277 | - | for i := 1; i < len(cards); i++ { | |
| 278 | - | if cards[i].Rect.X != cards[0].Rect.X || cards[i].Rect.Y < cards[i-1].Rect.Y+cards[i-1].Rect.H { | |
| 279 | - | t.Fatalf("cards not stacked down: %v then %v", cards[i-1].Rect, cards[i].Rect) | |
| 252 | + | col := []Placement{cards[(at+2)%3], cards[at], cards[(at+1)%3]} | |
| 253 | + | for i := 1; i < len(col); i++ { | |
| 254 | + | if col[i].Rect.X != col[0].Rect.X || col[i].Rect.Y < col[i-1].Rect.Y+col[i-1].Rect.H { | |
| 255 | + | t.Fatalf("cards not stacked down: %v then %v", col[i-1].Rect, col[i].Rect) | |
| 280 | 256 | } | |
| 281 | 257 | } | |
| 282 | - | m.OverviewMove(0, -1) | |
| 283 | - | m.OverviewMove(0, -1) | |
| 284 | - | if m.cardAt(w) != 0 { | |
| 285 | - | t.Fatalf("up: card %d, want 0", m.cardAt(w)) | |
| 258 | + | if !col[1].Focused { | |
| 259 | + | t.Fatal("the selection is not in the middle") | |
| 286 | 260 | } | |
| 287 | 261 | m.OverviewMove(0, 1) | |
| 288 | - | if m.cardAt(w) != 1 { | |
| 289 | - | t.Fatalf("down: card %d, want 1", m.cardAt(w)) | |
| 262 | + | if want := (at + 1) % 3; m.cardAt(w) != want { | |
| 263 | + | t.Fatalf("down: card %d, want %d", m.cardAt(w), want) | |
| 264 | + | } | |
| 265 | + | m.OverviewMove(0, -1) | |
| 266 | + | m.OverviewMove(0, -1) | |
| 267 | + | if want := (at + 2) % 3; m.cardAt(w) != want { | |
| 268 | + | t.Fatalf("up twice: card %d, want %d", m.cardAt(w), want) | |
| 290 | 269 | } | |
| 291 | 270 | m.OverviewMove(1, 0) | |
| 292 | 271 | if m.cardAt(w) >= 0 || m.overviewTarget() == 0 { | |
| 293 | 272 | t.Fatalf("right: card %d, target %d", m.cardAt(w), m.overviewTarget()) | |
| 294 | 273 | } | |
| 295 | 274 | } | |
| 275 | + | ||
| 276 | + | // A stash-only workspace's carousel does not wrap round, drawn or walked: | |
| 277 | + | // the selection stays in the middle, the cards in stash order on either | |
| 278 | + | // side of it. Past the ends a vertical overview stops; a cascade one | |
| 279 | + | // changes workspace. | |
| 280 | + | func TestOverviewStashOnlyCarousel(t *testing.T) { | |
| 281 | + | for _, axis := range []layoutAxis{horizontalAxis, verticalAxis} { | |
| 282 | + | t.Run(map[layoutAxis]string{horizontalAxis: "cascade", verticalAxis: "scroll"}[axis], func(t *testing.T) { | |
| 283 | + | m := overviewMonitor() | |
| 284 | + | m.Focus(2) | |
| 285 | + | m.AddWindow(30) | |
| 286 | + | m.Focus(1) | |
| 287 | + | w := m.Current() | |
| 288 | + | w.Overflow = OverflowCascade | |
| 289 | + | if axis == verticalAxis { | |
| 290 | + | w.Overflow = OverflowScroll | |
| 291 | + | } | |
| 292 | + | w.ToggleWindowStash() // its own window | |
| 293 | + | for id := WindowID(40); id < 44; id++ { | |
| 294 | + | w.AddWindow(id) | |
| 295 | + | w.FocusID(id) | |
| 296 | + | w.ToggleWindowStash() | |
| 297 | + | } | |
| 298 | + | m.ToggleOverview() | |
| 299 | + | if m.overviewAxis() != axis || len(w.Columns) != 0 || m.cardAt(w) < 0 { | |
| 300 | + | t.Fatalf("setup: axis %d, %d columns, card %d", m.overviewAxis(), len(w.Columns), m.cardAt(w)) | |
| 301 | + | } | |
| 302 | + | last := len(w.Stash) - 1 | |
| 303 | + | m.selectCard(w, last) | |
| 304 | + | ps := m.Layout() | |
| 305 | + | pos := func(i int) int { | |
| 306 | + | if r := previewOf(t, ps, w.Stash[i].ID).Rect; axis == verticalAxis { | |
| 307 | + | return r.Y | |
| 308 | + | } else { | |
| 309 | + | return r.X | |
| 310 | + | } | |
| 311 | + | } | |
| 312 | + | for i := 1; i <= last; i++ { | |
| 313 | + | if pos(i) <= pos(i-1) { | |
| 314 | + | t.Fatalf("card %d at %d, not after card %d at %d", i, pos(i), i-1, pos(i-1)) | |
| 315 | + | } | |
| 316 | + | } | |
| 317 | + | if axis == verticalAxis { | |
| 318 | + | m.OverviewMove(0, 1) | |
| 319 | + | if m.Current() != w || m.cardAt(w) != last { | |
| 320 | + | t.Fatalf("down past the last card: card %d", m.cardAt(w)) | |
| 321 | + | } | |
| 322 | + | return | |
| 323 | + | } | |
| 324 | + | m.OverviewMove(1, 0) | |
| 325 | + | if m.Current() == w { | |
| 326 | + | t.Fatal("right past the last card stayed on the workspace") | |
| 327 | + | } | |
| 328 | + | }) | |
| 329 | + | } | |
| 330 | + | } | |
| @@ -144,86 +144,58 @@ func TestOverviewCascadeStashFolds(t *testing.T) { | |||||
| 144 | } | 144 | } | ||
| 145 | } | 145 | } | ||
| 146 | 146 | ||||
| 147 | - | // A vertical overview's stash longer than the area scrolls like a wide | 147 | + | // A carousel longer than the area keeps every selection centred in it, |
| 148 | - | // row: every card is reached in order, the selection stays inside the area | 148 | + | // with cards on both sides, on either axis; the cards past the area are |
| 149 | - | // and is centred unless the strip stops at an edge of it, and cards past | 149 | + | // hidden. |
| 150 | - | // the area are hidden. | 150 | + | func TestOverviewCarouselScrolls(t *testing.T) { |
| 151 | - | func TestOverviewStashUnfoldScrolls(t *testing.T) { | 151 | + | for _, axis := range []layoutAxis{horizontalAxis, verticalAxis} { |
| 152 | - | m := cascadeOverview() | 152 | + | t.Run(map[layoutAxis]string{horizontalAxis: "cascade", verticalAxis: "scroll"}[axis], func(t *testing.T) { |
| 153 | - | w := m.Current() | 153 | + | m := cascadeOverview() |
| 154 | - | w.Overflow = OverflowScroll | 154 | + | w := m.Current() |
| 155 | - | withStash(m, 12) | 155 | + | if axis == verticalAxis { |
| 156 | - | m.ToggleOverview() | 156 | + | w.Overflow = OverflowScroll |
| 157 | - | if m.overviewAxis() != verticalAxis { | |||
| 158 | - | t.Fatal("setup: wrong overview axis") | |||
| 159 | - | } | |||
| 160 | - | m.selectCard(w, 0) | |||
| 161 | - | u := w.overviewArea() | |||
| 162 | - | lo, hi := u.Y, u.Y+u.H | |||
| 163 | - | centred, hidden := false, false | |||
| 164 | - | for i := range len(w.Stash) { | |||
| 165 | - | if m.cardAt(w) != i { | |||
| 166 | - | t.Fatalf("step %d: card %d", i, m.cardAt(w)) | |||
| 167 | - | } | |||
| 168 | - | ps := m.Layout() | |||
| 169 | - | sel := previewOf(t, ps, w.Stash[i].ID) | |||
| 170 | - | at, size := sel.Rect.Y, sel.Rect.H | |||
| 171 | - | if sel.Hidden || !sel.Focused || at < lo || at+size > hi { | |||
| 172 | - | t.Fatalf("card %d off the area: %+v in %v", i, sel, u) | |||
| 173 | - | } | |||
| 174 | - | first := previewOf(t, ps, w.Stash[0].ID).Rect.Y | |||
| 175 | - | last := previewOf(t, ps, w.Stash[len(w.Stash)-1].ID).Rect.Y | |||
| 176 | - | if first > lo || last+size < hi { | |||
| 177 | - | t.Fatalf("card %d: strip %d..%d does not fill the area %d..%d", i, first, last+size, lo, hi) | |||
| 178 | - | } | |||
| 179 | - | if mid := at + size/2; abs(mid-(lo+hi)/2) <= 1 { | |||
| 180 | - | centred = true | |||
| 181 | - | } else if first != lo && last+size != hi { | |||
| 182 | - | t.Fatalf("card %d at %d, neither centred on %d nor clamped", i, mid, (lo+hi)/2) | |||
| 183 | - | } | |||
| 184 | - | for _, e := range w.Stash { | |||
| 185 | - | hidden = hidden || previewOf(t, ps, e.ID).Hidden | |||
| 186 | - | } | |||
| 187 | - | m.OverviewMove(0, 1) | |||
| 188 | - | } | |||
| 189 | - | if !hidden || !centred { | |||
| 190 | - | t.Fatalf("12 cards: hidden %v, centred %v; the test does not scroll", hidden, centred) | |||
| 191 | - | } | |||
| 192 | - | } | |||
| 193 | - | ||||
| 194 | - | // A carousel wider than the area keeps every selection centred in it, with | |||
| 195 | - | // cards on both sides; the cards past the area are hidden. | |||
| 196 | - | func TestOverviewCascadeCarouselScrolls(t *testing.T) { | |||
| 197 | - | m := cascadeOverview() | |||
| 198 | - | w := m.Current() | |||
| 199 | - | withStash(m, 12) | |||
| 200 | - | m.ToggleOverview() | |||
| 201 | - | if m.overviewAxis() != horizontalAxis { | |||
| 202 | - | t.Fatal("setup: wrong overview axis") | |||
| 203 | - | } | |||
| 204 | - | m.selectCard(w, 11) | |||
| 205 | - | u := w.overviewArea() | |||
| 206 | - | for step := range len(w.Stash) + 1 { | |||
| 207 | - | i := m.cardAt(w) | |||
| 208 | - | ps := m.Layout() | |||
| 209 | - | sel := previewOf(t, ps, w.Stash[i].ID) | |||
| 210 | - | if sel.Hidden || !sel.Focused || abs(sel.Rect.X+sel.Rect.W/2-(u.X+u.W/2)) > 1 { | |||
| 211 | - | t.Fatalf("step %d: card %d not centred: %+v in %v", step, i, sel, u) | |||
| 212 | - | } | |||
| 213 | - | left, right, hidden := false, false, false | |||
| 214 | - | for _, e := range w.Stash { | |||
| 215 | - | p := previewOf(t, ps, e.ID) | |||
| 216 | - | if p.Hidden { | |||
| 217 | - | hidden = true | |||
| 218 | - | continue | |||
| 219 | } | 157 | } | ||
| 220 | - | left = left || p.Rect.X < sel.Rect.X | 158 | + | withStash(m, 12) |
| 221 | - | right = right || p.Rect.X > sel.Rect.X | 159 | + | m.ToggleOverview() |
| 222 | - | } | 160 | + | if m.overviewAxis() != axis { |
| 223 | - | if !left || !right || !hidden { | 161 | + | t.Fatal("setup: wrong overview axis") |
| 224 | - | t.Fatalf("step %d: cards left %v, right %v, hidden %v", step, left, right, hidden) | 162 | + | } |
| 225 | - | } | 163 | + | m.selectCard(w, 11) |
| 226 | - | m.OverviewMove(1, 0) | 164 | + | u := w.overviewArea() |
| 165 | + | pos := func(r Rect) (int, int) { | |||
| 166 | + | if axis == verticalAxis { | |||
| 167 | + | return r.Y, r.Y + r.H/2 - (u.Y + u.H/2) | |||
| 168 | + | } | |||
| 169 | + | return r.X, r.X + r.W/2 - (u.X + u.W/2) | |||
| 170 | + | } | |||
| 171 | + | for step := range len(w.Stash) + 1 { | |||
| 172 | + | i := m.cardAt(w) | |||
| 173 | + | ps := m.Layout() | |||
| 174 | + | sel := previewOf(t, ps, w.Stash[i].ID) | |||
| 175 | + | at, off := pos(sel.Rect) | |||
| 176 | + | if sel.Hidden || !sel.Focused || abs(off) > 1 { | |||
| 177 | + | t.Fatalf("step %d: card %d not centred: %+v in %v", step, i, sel, u) | |||
| 178 | + | } | |||
| 179 | + | before, after, hidden := false, false, false | |||
| 180 | + | for _, e := range w.Stash { | |||
| 181 | + | p := previewOf(t, ps, e.ID) | |||
| 182 | + | if p.Hidden { | |||
| 183 | + | hidden = true | |||
| 184 | + | continue | |||
| 185 | + | } | |||
| 186 | + | q, _ := pos(p.Rect) | |||
| 187 | + | before, after = before || q < at, after || q > at | |||
| 188 | + | } | |||
| 189 | + | if !before || !after || !hidden { | |||
| 190 | + | t.Fatalf("step %d: cards before %v, after %v, hidden %v", step, before, after, hidden) | |||
| 191 | + | } | |||
| 192 | + | if axis == verticalAxis { | |||
| 193 | + | m.OverviewMove(0, 1) | |||
| 194 | + | } else { | |||
| 195 | + | m.OverviewMove(1, 0) | |||
| 196 | + | } | |||
| 197 | + | } | |||
| 198 | + | }) | |||
| 227 | } | 199 | } | ||
| 228 | } | 200 | } | ||
| 229 | 201 | ||||
| @@ -251,8 +223,10 @@ func TestOverviewStashLayoutAllocations(t *testing.T) { | |||||
| 251 | } | 223 | } | ||
| 252 | } | 224 | } | ||
| 253 | 225 | ||||
| 254 | - | // A scroll overview unfolds its pile into a column on the left of the row; | 226 | + | // A scroll overview unfolds its pile into a carousel on the left of the |
| 255 | - | // up and down walk it, right goes back to the columns. | 227 | + | // row: a column centred on the selection, the card before it above and the |
| 228 | + | // one after it below, wrapping round. Up and down walk it round, right goes | |||
| 229 | + | // back to the columns. | |||
| 256 | func TestOverviewScrollStashUnfolds(t *testing.T) { | 230 | func TestOverviewScrollStashUnfolds(t *testing.T) { | ||
| 257 | m := overviewMonitor() | 231 | m := overviewMonitor() | ||
| 258 | w := m.Current() | 232 | w := m.Current() | ||
| @@ -267,29 +241,90 @@ func TestOverviewScrollStashUnfolds(t *testing.T) { | |||||
| 267 | t.Fatal("setup: not a vertical overview") | 241 | t.Fatal("setup: not a vertical overview") | ||
| 268 | } | 242 | } | ||
| 269 | m.OverviewMove(-1, 0) | 243 | m.OverviewMove(-1, 0) | ||
| 270 | - | if m.cardAt(w) < 0 { | 244 | + | at := m.cardAt(w) |
| 245 | + | if at < 0 { | |||
| 271 | t.Fatal("left from the first column did not select the stash") | 246 | t.Fatal("left from the first column did not select the stash") | ||
| 272 | } | 247 | } | ||
| 273 | cards := shownCards(t, w, m.Layout()) | 248 | cards := shownCards(t, w, m.Layout()) | ||
| 274 | if len(cards) != 3 { | 249 | if len(cards) != 3 { | ||
| 275 | t.Fatalf("%d cards shown, want 3", len(cards)) | 250 | t.Fatalf("%d cards shown, want 3", len(cards)) | ||
| 276 | } | 251 | } | ||
| 277 | - | for i := 1; i < len(cards); i++ { | 252 | + | col := []Placement{cards[(at+2)%3], cards[at], cards[(at+1)%3]} |
| 278 | - | if cards[i].Rect.X != cards[0].Rect.X || cards[i].Rect.Y < cards[i-1].Rect.Y+cards[i-1].Rect.H { | 253 | + | for i := 1; i < len(col); i++ { |
| 279 | - | t.Fatalf("cards not stacked down: %v then %v", cards[i-1].Rect, cards[i].Rect) | 254 | + | if col[i].Rect.X != col[0].Rect.X || col[i].Rect.Y < col[i-1].Rect.Y+col[i-1].Rect.H { |
| 255 | + | t.Fatalf("cards not stacked down: %v then %v", col[i-1].Rect, col[i].Rect) | |||
| 280 | } | 256 | } | ||
| 281 | } | 257 | } | ||
| 282 | - | m.OverviewMove(0, -1) | 258 | + | if !col[1].Focused { |
| 283 | - | m.OverviewMove(0, -1) | 259 | + | t.Fatal("the selection is not in the middle") |
| 284 | - | if m.cardAt(w) != 0 { | |||
| 285 | - | t.Fatalf("up: card %d, want 0", m.cardAt(w)) | |||
| 286 | } | 260 | } | ||
| 287 | m.OverviewMove(0, 1) | 261 | m.OverviewMove(0, 1) | ||
| 288 | - | if m.cardAt(w) != 1 { | 262 | + | if want := (at + 1) % 3; m.cardAt(w) != want { |
| 289 | - | t.Fatalf("down: card %d, want 1", m.cardAt(w)) | 263 | + | t.Fatalf("down: card %d, want %d", m.cardAt(w), want) |
| 264 | + | } | |||
| 265 | + | m.OverviewMove(0, -1) | |||
| 266 | + | m.OverviewMove(0, -1) | |||
| 267 | + | if want := (at + 2) % 3; m.cardAt(w) != want { | |||
| 268 | + | t.Fatalf("up twice: card %d, want %d", m.cardAt(w), want) | |||
| 290 | } | 269 | } | ||
| 291 | m.OverviewMove(1, 0) | 270 | m.OverviewMove(1, 0) | ||
| 292 | if m.cardAt(w) >= 0 || m.overviewTarget() == 0 { | 271 | if m.cardAt(w) >= 0 || m.overviewTarget() == 0 { | ||
| 293 | t.Fatalf("right: card %d, target %d", m.cardAt(w), m.overviewTarget()) | 272 | t.Fatalf("right: card %d, target %d", m.cardAt(w), m.overviewTarget()) | ||
| 294 | } | 273 | } | ||
| 295 | } | 274 | } | ||
| 275 | + | ||||
| 276 | + | // A stash-only workspace's carousel does not wrap round, drawn or walked: | |||
| 277 | + | // the selection stays in the middle, the cards in stash order on either | |||
| 278 | + | // side of it. Past the ends a vertical overview stops; a cascade one | |||
| 279 | + | // changes workspace. | |||
| 280 | + | func TestOverviewStashOnlyCarousel(t *testing.T) { | |||
| 281 | + | for _, axis := range []layoutAxis{horizontalAxis, verticalAxis} { | |||
| 282 | + | t.Run(map[layoutAxis]string{horizontalAxis: "cascade", verticalAxis: "scroll"}[axis], func(t *testing.T) { | |||
| 283 | + | m := overviewMonitor() | |||
| 284 | + | m.Focus(2) | |||
| 285 | + | m.AddWindow(30) | |||
| 286 | + | m.Focus(1) | |||
| 287 | + | w := m.Current() | |||
| 288 | + | w.Overflow = OverflowCascade | |||
| 289 | + | if axis == verticalAxis { | |||
| 290 | + | w.Overflow = OverflowScroll | |||
| 291 | + | } | |||
| 292 | + | w.ToggleWindowStash() // its own window | |||
| 293 | + | for id := WindowID(40); id < 44; id++ { | |||
| 294 | + | w.AddWindow(id) | |||
| 295 | + | w.FocusID(id) | |||
| 296 | + | w.ToggleWindowStash() | |||
| 297 | + | } | |||
| 298 | + | m.ToggleOverview() | |||
| 299 | + | if m.overviewAxis() != axis || len(w.Columns) != 0 || m.cardAt(w) < 0 { | |||
| 300 | + | t.Fatalf("setup: axis %d, %d columns, card %d", m.overviewAxis(), len(w.Columns), m.cardAt(w)) | |||
| 301 | + | } | |||
| 302 | + | last := len(w.Stash) - 1 | |||
| 303 | + | m.selectCard(w, last) | |||
| 304 | + | ps := m.Layout() | |||
| 305 | + | pos := func(i int) int { | |||
| 306 | + | if r := previewOf(t, ps, w.Stash[i].ID).Rect; axis == verticalAxis { | |||
| 307 | + | return r.Y | |||
| 308 | + | } else { | |||
| 309 | + | return r.X | |||
| 310 | + | } | |||
| 311 | + | } | |||
| 312 | + | for i := 1; i <= last; i++ { | |||
| 313 | + | if pos(i) <= pos(i-1) { | |||
| 314 | + | t.Fatalf("card %d at %d, not after card %d at %d", i, pos(i), i-1, pos(i-1)) | |||
| 315 | + | } | |||
| 316 | + | } | |||
| 317 | + | if axis == verticalAxis { | |||
| 318 | + | m.OverviewMove(0, 1) | |||
| 319 | + | if m.Current() != w || m.cardAt(w) != last { | |||
| 320 | + | t.Fatalf("down past the last card: card %d", m.cardAt(w)) | |||
| 321 | + | } | |||
| 322 | + | return | |||
| 323 | + | } | |||
| 324 | + | m.OverviewMove(1, 0) | |||
| 325 | + | if m.Current() == w { | |||
| 326 | + | t.Fatal("right past the last card stayed on the workspace") | |||
| 327 | + | } | |||
| 328 | + | }) | |||
| 329 | + | } | |||
| 330 | + | } | |||
M internal/core/transition.go +7 -7
| @@ -45,8 +45,8 @@ type viewShot struct { | |||
| 45 | 45 | stashShown bool | |
| 46 | 46 | stashAt, stashLen int | |
| 47 | 47 | stashPos, stashV float64 | |
| 48 | - | // card is the selected card of an unfolded stash carousel in the | |
| 49 | - | // preview (Workspace.carousel), -1 without one. | |
| 48 | + | // card is the selected card of the unfolded stash carousel in the | |
| 49 | + | // overview (Workspace.unfold), -1 without one. | |
| 50 | 50 | card int | |
| 51 | 51 | // list and pos are the numbered workspaces and the monitor's fractional | |
| 52 | 52 | // position in them (index of the current one plus switchView.off); ok is | |
| @@ -640,7 +640,7 @@ func (c *Core) snapshot(now time.Time) []viewShot { | |||
| 640 | 640 | _, s.switchV = m.switchView.motion.sampleAt(now) | |
| 641 | 641 | } | |
| 642 | 642 | s.stashShown, s.stashAt, s.stashLen, s.stashPos = w.stashShown(), w.stashAt, len(w.Stash), float64(w.stashAt)+w.stashView.off | |
| 643 | - | s.card = m.carouselCard(w) | |
| 643 | + | s.card = m.cardAt(w) | |
| 644 | 644 | if w.stashView.motion.on { | |
| 645 | 645 | _, s.stashV = w.stashView.motion.sampleAt(now) | |
| 646 | 646 | } | |
| @@ -918,7 +918,7 @@ func (c *Core) transitionOverview(b *viewShot, now time.Time) { | |||
| 918 | 918 | } | |
| 919 | 919 | rm := sc.rects[p.ID] | |
| 920 | 920 | i := slices.IndexFunc(b.rects, func(r rectShot) bool { return r.id == p.ID }) | |
| 921 | - | if i >= 0 && at >= 0 && wrapped(w.stashIndex(p.ID), b.card, at, len(w.Stash)) { | |
| 921 | + | if i >= 0 && at >= 0 && wrapped(w.stashIndex(p.ID), b.card, at, len(w.Stash), w.wrapsCards()) { | |
| 922 | 922 | // The card went round the carousel to its other end: it | |
| 923 | 923 | // fades in there instead of crossing the row. | |
| 924 | 924 | rm, i = rectMotion{}, -1 | |
| @@ -957,14 +957,14 @@ func carouselStep(b *viewShot) int { | |||
| 957 | 957 | if b.card < 0 || w != b.ws || len(w.Stash) != b.stashLen { | |
| 958 | 958 | return -1 | |
| 959 | 959 | } | |
| 960 | - | return m.carouselCard(w) | |
| 960 | + | return m.cardAt(w) | |
| 961 | 961 | } | |
| 962 | 962 | ||
| 963 | 963 | // wrapped reports whether stash card i of a carousel of n cards went round | |
| 964 | 964 | // it, from one end to the other: it moved more than one place | |
| 965 | 965 | // (carouselSlot) when the selection went from card from to card to. | |
| 966 | - | func wrapped(i, from, to, n int) bool { | |
| 967 | - | return i >= 0 && abs(carouselSlot(i, to, n)-carouselSlot(i, from, n)) > 1 | |
| 966 | + | func wrapped(i, from, to, n int, wraps bool) bool { | |
| 967 | + | return i >= 0 && abs(carouselSlot(i, to, n, wraps)-carouselSlot(i, from, n, wraps)) > 1 | |
| 968 | 968 | } | |
| 969 | 969 | ||
| 970 | 970 | // movedFrom is the shot of the other screen that showed the workspace b's | |
| @@ -45,8 +45,8 @@ type viewShot struct { | |||||
| 45 | stashShown bool | 45 | stashShown bool | ||
| 46 | stashAt, stashLen int | 46 | stashAt, stashLen int | ||
| 47 | stashPos, stashV float64 | 47 | stashPos, stashV float64 | ||
| 48 | - | // card is the selected card of an unfolded stash carousel in the | 48 | + | // card is the selected card of the unfolded stash carousel in the |
| 49 | - | // preview (Workspace.carousel), -1 without one. | 49 | + | // overview (Workspace.unfold), -1 without one. |
| 50 | card int | 50 | card int | ||
| 51 | // list and pos are the numbered workspaces and the monitor's fractional | 51 | // list and pos are the numbered workspaces and the monitor's fractional | ||
| 52 | // position in them (index of the current one plus switchView.off); ok is | 52 | // position in them (index of the current one plus switchView.off); ok is | ||
| @@ -640,7 +640,7 @@ func (c *Core) snapshot(now time.Time) []viewShot { | |||||
| 640 | _, s.switchV = m.switchView.motion.sampleAt(now) | 640 | _, s.switchV = m.switchView.motion.sampleAt(now) | ||
| 641 | } | 641 | } | ||
| 642 | s.stashShown, s.stashAt, s.stashLen, s.stashPos = w.stashShown(), w.stashAt, len(w.Stash), float64(w.stashAt)+w.stashView.off | 642 | s.stashShown, s.stashAt, s.stashLen, s.stashPos = w.stashShown(), w.stashAt, len(w.Stash), float64(w.stashAt)+w.stashView.off | ||
| 643 | - | s.card = m.carouselCard(w) | 643 | + | s.card = m.cardAt(w) |
| 644 | if w.stashView.motion.on { | 644 | if w.stashView.motion.on { | ||
| 645 | _, s.stashV = w.stashView.motion.sampleAt(now) | 645 | _, s.stashV = w.stashView.motion.sampleAt(now) | ||
| 646 | } | 646 | } | ||
| @@ -918,7 +918,7 @@ func (c *Core) transitionOverview(b *viewShot, now time.Time) { | |||||
| 918 | } | 918 | } | ||
| 919 | rm := sc.rects[p.ID] | 919 | rm := sc.rects[p.ID] | ||
| 920 | i := slices.IndexFunc(b.rects, func(r rectShot) bool { return r.id == p.ID }) | 920 | i := slices.IndexFunc(b.rects, func(r rectShot) bool { return r.id == p.ID }) | ||
| 921 | - | if i >= 0 && at >= 0 && wrapped(w.stashIndex(p.ID), b.card, at, len(w.Stash)) { | 921 | + | if i >= 0 && at >= 0 && wrapped(w.stashIndex(p.ID), b.card, at, len(w.Stash), w.wrapsCards()) { |
| 922 | // The card went round the carousel to its other end: it | 922 | // The card went round the carousel to its other end: it | ||
| 923 | // fades in there instead of crossing the row. | 923 | // fades in there instead of crossing the row. | ||
| 924 | rm, i = rectMotion{}, -1 | 924 | rm, i = rectMotion{}, -1 | ||
| @@ -957,14 +957,14 @@ func carouselStep(b *viewShot) int { | |||||
| 957 | if b.card < 0 || w != b.ws || len(w.Stash) != b.stashLen { | 957 | if b.card < 0 || w != b.ws || len(w.Stash) != b.stashLen { | ||
| 958 | return -1 | 958 | return -1 | ||
| 959 | } | 959 | } | ||
| 960 | - | return m.carouselCard(w) | 960 | + | return m.cardAt(w) |
| 961 | } | 961 | } | ||
| 962 | 962 | ||||
| 963 | // wrapped reports whether stash card i of a carousel of n cards went round | 963 | // wrapped reports whether stash card i of a carousel of n cards went round | ||
| 964 | // it, from one end to the other: it moved more than one place | 964 | // it, from one end to the other: it moved more than one place | ||
| 965 | // (carouselSlot) when the selection went from card from to card to. | 965 | // (carouselSlot) when the selection went from card from to card to. | ||
| 966 | - | func wrapped(i, from, to, n int) bool { | 966 | + | func wrapped(i, from, to, n int, wraps bool) bool { |
| 967 | - | return i >= 0 && abs(carouselSlot(i, to, n)-carouselSlot(i, from, n)) > 1 | 967 | + | return i >= 0 && abs(carouselSlot(i, to, n, wraps)-carouselSlot(i, from, n, wraps)) > 1 |
| 968 | } | 968 | } | ||
| 969 | 969 | ||||
| 970 | // movedFrom is the shot of the other screen that showed the workspace b's | 970 | // movedFrom is the shot of the other screen that showed the workspace b's | ||