fix(overview): fade only the carousel cards that cross the seam and clamp long stash-only carousels
- commit
- 6648883e76386bd3bef0a4be61d6d99f0c1d99f0
- author
- brice · · 2026-10-16 12:42 UTC
- parent
- a82be5b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +144 -76
M internal/core/overview.go +26 -23
| @@ -402,9 +402,10 @@ func (m *Monitor) OverviewMove(dx, dy int) { | |||
| 402 | 402 | } | |
| 403 | 403 | ||
| 404 | 404 | // overviewWorkspaceStep is a focus-workspace bind. A vertical overview | |
| 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. | |
| 405 | + | // walks the stack cards before changing workspace (overviewVertical); from | |
| 406 | + | // a selected stash card, which the move keys walk, it changes workspace | |
| 407 | + | // directly. A side-by-side one always changes workspace, since its stack | |
| 408 | + | // and unfolded stash run left and right. | |
| 408 | 409 | func (m *Monitor) overviewWorkspaceStep(d int) { | |
| 409 | 410 | if m.overviewAxis() == verticalAxis && m.cardAt(m.Current()) < 0 { | |
| 410 | 411 | m.overviewVertical(d) | |
| @@ -414,16 +415,14 @@ func (m *Monitor) overviewWorkspaceStep(d int) { | |||
| 414 | 415 | } | |
| 415 | 416 | ||
| 416 | 417 | // cycleCard selects the stash card d entries from the selected one of w in | |
| 417 | - | // the unfolded pile, wrapping round (wrapsCards), else stopping at either | |
| 418 | + | // the unfolded pile, wrapping round (wrapsCards), else staying at either | |
| 418 | 419 | // end. It reports whether a card was selected. | |
| 419 | 420 | func (m *Monitor) cycleCard(w *Workspace, d int) bool { | |
| 420 | 421 | at := m.cardAt(w) | |
| 421 | 422 | if at < 0 { | |
| 422 | 423 | return false | |
| 423 | 424 | } | |
| 424 | - | if !m.stepCard(w, at, d) { | |
| 425 | - | m.selectCard(w, min(max(at+d, 0), len(w.Stash)-1)) | |
| 426 | - | } | |
| 425 | + | m.stepCard(w, at, d) | |
| 427 | 426 | return true | |
| 428 | 427 | } | |
| 429 | 428 | ||
| @@ -510,10 +509,8 @@ func (m *Monitor) selectLaneColumn(w *Workspace, i int) { | |||
| 510 | 509 | // side by side (cascade): a column inside the selected lane, or a card of | |
| 511 | 510 | // the unfolded stash, which wraps round (stepCard). Past the first column | |
| 512 | 511 | // or a card that does not wrap, it goes to the previous workspace; past | |
| 513 | - | // the last, to the next workspace. | |
| 514 | - | // Covering floats fan | |
| 515 | - | // along this axis and are traversed before a workspace: the card on the | |
| 516 | - | // left is the one behind. | |
| 512 | + | // the last, to the next workspace. Covering floats fan along this axis and | |
| 513 | + | // are traversed before a workspace: the card on the left is the one behind. | |
| 517 | 514 | func (m *Monitor) overviewAcross(dx int) { | |
| 518 | 515 | w := m.Current() | |
| 519 | 516 | if at := m.cardAt(w); at >= 0 { | |
| @@ -949,9 +946,11 @@ func (w *Workspace) pile(dst []Placement, axis layoutAxis, slot Rect, dim bool, | |||
| 949 | 946 | // vertical one. The selected card at sits on the pile, in the middle; the | |
| 950 | 947 | // cards before it come on the left (above), the ones after it on the right | |
| 951 | 948 | // (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. | |
| 949 | + | // following the newest (carouselSlot). A wider gap marks that seam. A | |
| 950 | + | // carousel that fits the overview area is shifted into it. Longer, one | |
| 951 | + | // that wraps keeps the selection in the middle; one that does not stops at | |
| 952 | + | // the area's edges, like a wide row. The cards past the area are hidden, | |
| 953 | + | // the ones other than at dimmed. The cards are appended to dst. | |
| 955 | 954 | func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int) []Placement { | |
| 956 | 955 | n := len(w.Stash) | |
| 957 | 956 | u := w.overviewArea() | |
| @@ -963,8 +962,7 @@ func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int | |||
| 963 | 962 | // The extra room at the seam, on top of the stride. | |
| 964 | 963 | gap := size / 4 | |
| 965 | 964 | wraps := w.wrapsCards() | |
| 966 | - | pos := func(i int) int { | |
| 967 | - | s := carouselSlot(i, at, n, wraps) | |
| 965 | + | pos := func(s int) int { | |
| 968 | 966 | p := origin + s*stride | |
| 969 | 967 | switch { | |
| 970 | 968 | case s > 0 && at+s >= n: | |
| @@ -974,20 +972,25 @@ func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int | |||
| 974 | 972 | } | |
| 975 | 973 | return p | |
| 976 | 974 | } | |
| 977 | - | first, last := pos(0), pos(n-1) | |
| 975 | + | // The slots of the ends. | |
| 976 | + | from, to := -at, n-1-at | |
| 978 | 977 | if wraps { | |
| 979 | - | first, last = pos((at+n-n/2)%n), pos((at+(n-1)/2)%n) | |
| 978 | + | from, to = -(n / 2), (n-1)/2 | |
| 980 | 979 | } | |
| 981 | - | shift := 0 | |
| 982 | - | if total := last + size - first; total <= room { | |
| 980 | + | first, total := pos(from), pos(to)+size-pos(from) | |
| 981 | + | var shift int | |
| 982 | + | switch { | |
| 983 | + | case total <= room: | |
| 983 | 984 | shift = min(max(first, lo), lo+room-total) - first | |
| 985 | + | case !wraps: | |
| 986 | + | shift = min(max(first, lo+room-total), lo) - first | |
| 984 | 987 | } | |
| 985 | 988 | for i := range w.Stash { | |
| 986 | 989 | r := folded | |
| 987 | - | if axis == verticalAxis { | |
| 988 | - | r.Y = pos(i) + shift | |
| 990 | + | if p := pos(carouselSlot(i, at, n, wraps)) + shift; axis == verticalAxis { | |
| 991 | + | r.Y = p | |
| 989 | 992 | } else { | |
| 990 | - | r.X = pos(i) + shift | |
| 993 | + | r.X = p | |
| 991 | 994 | } | |
| 992 | 995 | dst = w.unfoldedCard(dst, i, r, at, u) | |
| 993 | 996 | } | |
| @@ -402,9 +402,10 @@ func (m *Monitor) OverviewMove(dx, dy int) { | |||||
| 402 | } | 402 | } | ||
| 403 | 403 | ||||
| 404 | // overviewWorkspaceStep is a focus-workspace bind. A vertical overview | 404 | // overviewWorkspaceStep is a focus-workspace bind. A vertical overview | ||
| 405 | - | // walks the stack cards before changing workspace (overviewVertical), not | 405 | + | // walks the stack cards before changing workspace (overviewVertical); from |
| 406 | - | // the unfolded stash, which wraps round. A side-by-side one always changes | 406 | + | // a selected stash card, which the move keys walk, it changes workspace |
| 407 | - | // workspace, since its stack and unfolded stash run left and right. | 407 | + | // directly. A side-by-side one always changes workspace, since its stack |
| 408 | + | // and unfolded stash run left and right. | |||
| 408 | func (m *Monitor) overviewWorkspaceStep(d int) { | 409 | func (m *Monitor) overviewWorkspaceStep(d int) { | ||
| 409 | if m.overviewAxis() == verticalAxis && m.cardAt(m.Current()) < 0 { | 410 | if m.overviewAxis() == verticalAxis && m.cardAt(m.Current()) < 0 { | ||
| 410 | m.overviewVertical(d) | 411 | m.overviewVertical(d) | ||
| @@ -414,16 +415,14 @@ func (m *Monitor) overviewWorkspaceStep(d int) { | |||||
| 414 | } | 415 | } | ||
| 415 | 416 | ||||
| 416 | // cycleCard selects the stash card d entries from the selected one of w in | 417 | // cycleCard selects the stash card d entries from the selected one of w in | ||
| 417 | - | // the unfolded pile, wrapping round (wrapsCards), else stopping at either | 418 | + | // the unfolded pile, wrapping round (wrapsCards), else staying at either |
| 418 | // end. It reports whether a card was selected. | 419 | // end. It reports whether a card was selected. | ||
| 419 | func (m *Monitor) cycleCard(w *Workspace, d int) bool { | 420 | func (m *Monitor) cycleCard(w *Workspace, d int) bool { | ||
| 420 | at := m.cardAt(w) | 421 | at := m.cardAt(w) | ||
| 421 | if at < 0 { | 422 | if at < 0 { | ||
| 422 | return false | 423 | return false | ||
| 423 | } | 424 | } | ||
| 424 | - | if !m.stepCard(w, at, d) { | 425 | + | m.stepCard(w, at, d) |
| 425 | - | m.selectCard(w, min(max(at+d, 0), len(w.Stash)-1)) | |||
| 426 | - | } | |||
| 427 | return true | 426 | return true | ||
| 428 | } | 427 | } | ||
| 429 | 428 | ||||
| @@ -510,10 +509,8 @@ func (m *Monitor) selectLaneColumn(w *Workspace, i int) { | |||||
| 510 | // side by side (cascade): a column inside the selected lane, or a card of | 509 | // side by side (cascade): a column inside the selected lane, or a card of | ||
| 511 | // the unfolded stash, which wraps round (stepCard). Past the first column | 510 | // 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 | 511 | // or a card that does not wrap, it goes to the previous workspace; past | ||
| 513 | - | // the last, to the next workspace. | 512 | + | // the last, to the next workspace. Covering floats fan along this axis and |
| 514 | - | // Covering floats fan | 513 | + | // are traversed before a workspace: the card on the left is the one behind. |
| 515 | - | // along this axis and are traversed before a workspace: the card on the | |||
| 516 | - | // left is the one behind. | |||
| 517 | func (m *Monitor) overviewAcross(dx int) { | 514 | func (m *Monitor) overviewAcross(dx int) { | ||
| 518 | w := m.Current() | 515 | w := m.Current() | ||
| 519 | if at := m.cardAt(w); at >= 0 { | 516 | if at := m.cardAt(w); at >= 0 { | ||
| @@ -949,9 +946,11 @@ func (w *Workspace) pile(dst []Placement, axis layoutAxis, slot Rect, dim bool, | |||||
| 949 | // vertical one. The selected card at sits on the pile, in the middle; the | 946 | // 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 | 947 | // 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 | 948 | // (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 | 949 | + | // following the newest (carouselSlot). A wider gap marks that seam. A |
| 953 | - | // overview area it is shifted into it; else the cards past the area are | 950 | + | // carousel that fits the overview area is shifted into it. Longer, one |
| 954 | - | // hidden. Cards other than at are dimmed. The cards are appended to dst. | 951 | + | // that wraps keeps the selection in the middle; one that does not stops at |
| 952 | + | // the area's edges, like a wide row. The cards past the area are hidden, | |||
| 953 | + | // the ones other than at dimmed. The cards are appended to dst. | |||
| 955 | func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int) []Placement { | 954 | func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int) []Placement { | ||
| 956 | n := len(w.Stash) | 955 | n := len(w.Stash) | ||
| 957 | u := w.overviewArea() | 956 | u := w.overviewArea() | ||
| @@ -963,8 +962,7 @@ func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int | |||||
| 963 | // The extra room at the seam, on top of the stride. | 962 | // The extra room at the seam, on top of the stride. | ||
| 964 | gap := size / 4 | 963 | gap := size / 4 | ||
| 965 | wraps := w.wrapsCards() | 964 | wraps := w.wrapsCards() | ||
| 966 | - | pos := func(i int) int { | 965 | + | pos := func(s int) int { |
| 967 | - | s := carouselSlot(i, at, n, wraps) | |||
| 968 | p := origin + s*stride | 966 | p := origin + s*stride | ||
| 969 | switch { | 967 | switch { | ||
| 970 | case s > 0 && at+s >= n: | 968 | case s > 0 && at+s >= n: | ||
| @@ -974,20 +972,25 @@ func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int | |||||
| 974 | } | 972 | } | ||
| 975 | return p | 973 | return p | ||
| 976 | } | 974 | } | ||
| 977 | - | first, last := pos(0), pos(n-1) | 975 | + | // The slots of the ends. |
| 976 | + | from, to := -at, n-1-at | |||
| 978 | if wraps { | 977 | if wraps { | ||
| 979 | - | first, last = pos((at+n-n/2)%n), pos((at+(n-1)/2)%n) | 978 | + | from, to = -(n / 2), (n-1)/2 |
| 980 | } | 979 | } | ||
| 981 | - | shift := 0 | 980 | + | first, total := pos(from), pos(to)+size-pos(from) |
| 982 | - | if total := last + size - first; total <= room { | 981 | + | var shift int |
| 982 | + | switch { | |||
| 983 | + | case total <= room: | |||
| 983 | shift = min(max(first, lo), lo+room-total) - first | 984 | shift = min(max(first, lo), lo+room-total) - first | ||
| 985 | + | case !wraps: | |||
| 986 | + | shift = min(max(first, lo+room-total), lo) - first | |||
| 984 | } | 987 | } | ||
| 985 | for i := range w.Stash { | 988 | for i := range w.Stash { | ||
| 986 | r := folded | 989 | r := folded | ||
| 987 | - | if axis == verticalAxis { | 990 | + | if p := pos(carouselSlot(i, at, n, wraps)) + shift; axis == verticalAxis { |
| 988 | - | r.Y = pos(i) + shift | 991 | + | r.Y = p |
| 989 | } else { | 992 | } else { | ||
| 990 | - | r.X = pos(i) + shift | 993 | + | r.X = p |
| 991 | } | 994 | } | ||
| 992 | dst = w.unfoldedCard(dst, i, r, at, u) | 995 | dst = w.unfoldedCard(dst, i, r, at, u) | ||
| 993 | } | 996 | } | ||
M internal/core/overview_stash_input_test.go +23 -15
| @@ -74,21 +74,29 @@ func TestOverviewStashInputNavigation(t *testing.T) { | |||
| 74 | 74 | } | |
| 75 | 75 | ||
| 76 | 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. | |
| 77 | + | // changes workspace, in its direction, wrapping round or not: the move | |
| 78 | + | // keys walk the cards. | |
| 79 | 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") | |
| 80 | + | for _, n := range []int{2, 3} { | |
| 81 | + | m := overviewMonitor() | |
| 82 | + | w := m.Current() | |
| 83 | + | for id := WindowID(5); id < WindowID(5+n); id++ { | |
| 84 | + | w.AddWindow(id) | |
| 85 | + | w.FocusID(id) | |
| 86 | + | w.ToggleWindowStash() | |
| 87 | + | } | |
| 88 | + | w.FocusID(1) | |
| 89 | + | m.Focus(1) | |
| 90 | + | m.AddWindow(30) | |
| 91 | + | m.Focus(0) | |
| 92 | + | m.ToggleOverview() | |
| 93 | + | m.OverviewMove(-1, 0) | |
| 94 | + | if m.overviewAxis() != verticalAxis || m.cardAt(w) < 0 { | |
| 95 | + | t.Fatalf("%d cards, setup: no card selected in a vertical overview", n) | |
| 96 | + | } | |
| 97 | + | m.overviewFocus(ActionFocusWorkspaceNext) | |
| 98 | + | if m.Current() != m.Workspaces[1] || m.cardAt(w) >= 0 { | |
| 99 | + | t.Fatalf("%d cards: focus-workspace-next went to workspace %d, card %d", n, indexOf(m.Workspaces, m.Current()), m.cardAt(w)) | |
| 100 | + | } | |
| 93 | 101 | } | |
| 94 | 102 | } | |
| @@ -74,21 +74,29 @@ func TestOverviewStashInputNavigation(t *testing.T) { | |||||
| 74 | } | 74 | } | ||
| 75 | 75 | ||||
| 76 | // In a vertical overview, a focus-workspace bind over the unfolded stash | 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 | 77 | + | // changes workspace, in its direction, wrapping round or not: the move |
| 78 | - | // get there. | 78 | + | // keys walk the cards. |
| 79 | func TestOverviewWorkspaceBindLeavesCarousel(t *testing.T) { | 79 | func TestOverviewWorkspaceBindLeavesCarousel(t *testing.T) { | ||
| 80 | - | m := pileMonitor() | 80 | + | for _, n := range []int{2, 3} { |
| 81 | - | w := m.Current() | 81 | + | m := overviewMonitor() |
| 82 | - | m.Focus(1) | 82 | + | w := m.Current() |
| 83 | - | m.AddWindow(30) | 83 | + | for id := WindowID(5); id < WindowID(5+n); id++ { |
| 84 | - | m.Focus(0) | 84 | + | w.AddWindow(id) |
| 85 | - | m.ToggleOverview() | 85 | + | w.FocusID(id) |
| 86 | - | m.OverviewMove(-1, 0) | 86 | + | w.ToggleWindowStash() |
| 87 | - | if m.overviewAxis() != verticalAxis || m.cardAt(w) < 0 { | 87 | + | } |
| 88 | - | t.Fatal("setup: no card selected in a vertical overview") | 88 | + | w.FocusID(1) |
| 89 | - | } | 89 | + | m.Focus(1) |
| 90 | - | m.overviewFocus(ActionFocusWorkspaceNext) | 90 | + | m.AddWindow(30) |
| 91 | - | if m.Current() == w { | 91 | + | m.Focus(0) |
| 92 | - | t.Fatal("focus-workspace-next over a card stayed on the workspace") | 92 | + | m.ToggleOverview() |
| 93 | + | m.OverviewMove(-1, 0) | |||
| 94 | + | if m.overviewAxis() != verticalAxis || m.cardAt(w) < 0 { | |||
| 95 | + | t.Fatalf("%d cards, setup: no card selected in a vertical overview", n) | |||
| 96 | + | } | |||
| 97 | + | m.overviewFocus(ActionFocusWorkspaceNext) | |||
| 98 | + | if m.Current() != m.Workspaces[1] || m.cardAt(w) >= 0 { | |||
| 99 | + | t.Fatalf("%d cards: focus-workspace-next went to workspace %d, card %d", n, indexOf(m.Workspaces, m.Current()), m.cardAt(w)) | |||
| 100 | + | } | |||
| 93 | } | 101 | } | ||
| 94 | } | 102 | } | ||
M internal/core/overview_unfold_test.go +31 -0
| @@ -328,3 +328,34 @@ func TestOverviewStashOnlyCarousel(t *testing.T) { | |||
| 328 | 328 | }) | |
| 329 | 329 | } | |
| 330 | 330 | } | |
| 331 | + | ||
| 332 | + | // A carousel that does not wrap round and is longer than the area stops at | |
| 333 | + | // its edges, like a wide row: the first card at the start of the area, the | |
| 334 | + | // selection inside it. | |
| 335 | + | func TestOverviewStashOnlyCarouselScrolls(t *testing.T) { | |
| 336 | + | m := overviewMonitor() | |
| 337 | + | m.Focus(1) | |
| 338 | + | w := m.Current() | |
| 339 | + | w.Overflow = OverflowCascade | |
| 340 | + | w.ToggleWindowStash() // its own window | |
| 341 | + | for id := WindowID(40); id < 52; id++ { | |
| 342 | + | w.AddWindow(id) | |
| 343 | + | w.FocusID(id) | |
| 344 | + | w.ToggleWindowStash() | |
| 345 | + | } | |
| 346 | + | m.ToggleOverview() | |
| 347 | + | if w.wrapsCards() || m.cardAt(w) < 0 { | |
| 348 | + | t.Fatalf("setup: wraps %v, card %d", w.wrapsCards(), m.cardAt(w)) | |
| 349 | + | } | |
| 350 | + | u := w.overviewArea() | |
| 351 | + | m.selectCard(w, 0) | |
| 352 | + | ps := m.Layout() | |
| 353 | + | if r := previewOf(t, ps, w.Stash[0].ID).Rect; r.X != u.X { | |
| 354 | + | t.Fatalf("first card at %d, area starts at %d", r.X, u.X) | |
| 355 | + | } | |
| 356 | + | m.selectCard(w, len(w.Stash)-1) | |
| 357 | + | ps = m.Layout() | |
| 358 | + | if r := previewOf(t, ps, w.Stash[len(w.Stash)-1].ID).Rect; 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 | + | } | |
| 361 | + | } | |
| @@ -328,3 +328,34 @@ func TestOverviewStashOnlyCarousel(t *testing.T) { | |||||
| 328 | }) | 328 | }) | ||
| 329 | } | 329 | } | ||
| 330 | } | 330 | } | ||
| 331 | + | ||||
| 332 | + | // A carousel that does not wrap round and is longer than the area stops at | |||
| 333 | + | // its edges, like a wide row: the first card at the start of the area, the | |||
| 334 | + | // selection inside it. | |||
| 335 | + | func TestOverviewStashOnlyCarouselScrolls(t *testing.T) { | |||
| 336 | + | m := overviewMonitor() | |||
| 337 | + | m.Focus(1) | |||
| 338 | + | w := m.Current() | |||
| 339 | + | w.Overflow = OverflowCascade | |||
| 340 | + | w.ToggleWindowStash() // its own window | |||
| 341 | + | for id := WindowID(40); id < 52; id++ { | |||
| 342 | + | w.AddWindow(id) | |||
| 343 | + | w.FocusID(id) | |||
| 344 | + | w.ToggleWindowStash() | |||
| 345 | + | } | |||
| 346 | + | m.ToggleOverview() | |||
| 347 | + | if w.wrapsCards() || m.cardAt(w) < 0 { | |||
| 348 | + | t.Fatalf("setup: wraps %v, card %d", w.wrapsCards(), m.cardAt(w)) | |||
| 349 | + | } | |||
| 350 | + | u := w.overviewArea() | |||
| 351 | + | m.selectCard(w, 0) | |||
| 352 | + | ps := m.Layout() | |||
| 353 | + | if r := previewOf(t, ps, w.Stash[0].ID).Rect; r.X != u.X { | |||
| 354 | + | t.Fatalf("first card at %d, area starts at %d", r.X, u.X) | |||
| 355 | + | } | |||
| 356 | + | m.selectCard(w, len(w.Stash)-1) | |||
| 357 | + | ps = m.Layout() | |||
| 358 | + | if r := previewOf(t, ps, w.Stash[len(w.Stash)-1].ID).Rect; 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 | + | } | |||
| 361 | + | } | |||
M internal/core/transition.go +14 -10
| @@ -46,8 +46,10 @@ type viewShot struct { | |||
| 46 | 46 | stashAt, stashLen int | |
| 47 | 47 | stashPos, stashV float64 | |
| 48 | 48 | // card is the selected card of the unfolded stash carousel in the | |
| 49 | - | // overview (Workspace.unfold), -1 without one. | |
| 50 | - | card int | |
| 49 | + | // overview (Workspace.unfold), -1 without one; wraps is whether that | |
| 50 | + | // carousel wrapped round (Workspace.wrapsCards). | |
| 51 | + | card int | |
| 52 | + | wraps bool | |
| 51 | 53 | // list and pos are the numbered workspaces and the monitor's fractional | |
| 52 | 54 | // position in them (index of the current one plus switchView.off); ok is | |
| 53 | 55 | // false when there is none (a shown stash workspace). | |
| @@ -640,7 +642,7 @@ func (c *Core) snapshot(now time.Time) []viewShot { | |||
| 640 | 642 | _, s.switchV = m.switchView.motion.sampleAt(now) | |
| 641 | 643 | } | |
| 642 | 644 | 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.cardAt(w) | |
| 645 | + | s.card, s.wraps = m.cardAt(w), w.wrapsCards() | |
| 644 | 646 | if w.stashView.motion.on { | |
| 645 | 647 | _, s.stashV = w.stashView.motion.sampleAt(now) | |
| 646 | 648 | } | |
| @@ -912,13 +914,14 @@ func (c *Core) transitionOverview(b *viewShot, now time.Time) { | |||
| 912 | 914 | at = carouselStep(b) | |
| 913 | 915 | } | |
| 914 | 916 | w := sc.mon.Current() | |
| 917 | + | n := len(w.Stash) | |
| 915 | 918 | for _, p := range sc.mon.Layout() { | |
| 916 | 919 | if p.Hidden { | |
| 917 | 920 | continue | |
| 918 | 921 | } | |
| 919 | 922 | rm := sc.rects[p.ID] | |
| 920 | 923 | 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), w.wrapsCards()) { | |
| 924 | + | if i >= 0 && at >= 0 && wrapped(w.stashIndex(p.ID), b.card, at, n, b.wraps) { | |
| 922 | 925 | // The card went round the carousel to its other end: it | |
| 923 | 926 | // fades in there instead of crossing the row. | |
| 924 | 927 | rm, i = rectMotion{}, -1 | |
| @@ -949,22 +952,23 @@ func (c *Core) transitionOverview(b *viewShot, now time.Time) { | |||
| 949 | 952 | } | |
| 950 | 953 | ||
| 951 | 954 | // carouselStep is the selected card of the stash carousel b's screen shows | |
| 952 | - | // now, when it showed the same carousel in b too (the same workspace and | |
| 953 | - | // stash size); else -1. | |
| 955 | + | // now, when it showed the same carousel in b too (the same workspace, stash | |
| 956 | + | // size and wrapping); else -1. | |
| 954 | 957 | func carouselStep(b *viewShot) int { | |
| 955 | 958 | m := b.sc.mon | |
| 956 | 959 | w := m.Current() | |
| 957 | - | if b.card < 0 || w != b.ws || len(w.Stash) != b.stashLen { | |
| 960 | + | if b.card < 0 || w != b.ws || len(w.Stash) != b.stashLen || w.wrapsCards() != b.wraps { | |
| 958 | 961 | return -1 | |
| 959 | 962 | } | |
| 960 | 963 | return m.cardAt(w) | |
| 961 | 964 | } | |
| 962 | 965 | ||
| 963 | 966 | // 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 | |
| 965 | - | // (carouselSlot) when the selection went from card from to card to. | |
| 967 | + | // it, from one end to the other, when the selection went from card from to | |
| 968 | + | // card to: it moved other than the cards that stayed on their side, which | |
| 969 | + | // all shift by the old selection's move (carouselSlot). | |
| 966 | 970 | 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 | |
| 971 | + | return i >= 0 && carouselSlot(i, to, n, wraps)-carouselSlot(i, from, n, wraps) != carouselSlot(from, to, n, wraps) | |
| 968 | 972 | } | |
| 969 | 973 | ||
| 970 | 974 | // movedFrom is the shot of the other screen that showed the workspace b's | |
| @@ -46,8 +46,10 @@ type viewShot struct { | |||||
| 46 | stashAt, stashLen int | 46 | stashAt, stashLen int | ||
| 47 | stashPos, stashV float64 | 47 | stashPos, stashV float64 | ||
| 48 | // card is the selected card of the unfolded stash carousel in the | 48 | // card is the selected card of the unfolded stash carousel in the | ||
| 49 | - | // overview (Workspace.unfold), -1 without one. | 49 | + | // overview (Workspace.unfold), -1 without one; wraps is whether that |
| 50 | - | card int | 50 | + | // carousel wrapped round (Workspace.wrapsCards). |
| 51 | + | card int | |||
| 52 | + | wraps bool | |||
| 51 | // list and pos are the numbered workspaces and the monitor's fractional | 53 | // 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 | 54 | // position in them (index of the current one plus switchView.off); ok is | ||
| 53 | // false when there is none (a shown stash workspace). | 55 | // false when there is none (a shown stash workspace). | ||
| @@ -640,7 +642,7 @@ func (c *Core) snapshot(now time.Time) []viewShot { | |||||
| 640 | _, s.switchV = m.switchView.motion.sampleAt(now) | 642 | _, s.switchV = m.switchView.motion.sampleAt(now) | ||
| 641 | } | 643 | } | ||
| 642 | s.stashShown, s.stashAt, s.stashLen, s.stashPos = w.stashShown(), w.stashAt, len(w.Stash), float64(w.stashAt)+w.stashView.off | 644 | 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.cardAt(w) | 645 | + | s.card, s.wraps = m.cardAt(w), w.wrapsCards() |
| 644 | if w.stashView.motion.on { | 646 | if w.stashView.motion.on { | ||
| 645 | _, s.stashV = w.stashView.motion.sampleAt(now) | 647 | _, s.stashV = w.stashView.motion.sampleAt(now) | ||
| 646 | } | 648 | } | ||
| @@ -912,13 +914,14 @@ func (c *Core) transitionOverview(b *viewShot, now time.Time) { | |||||
| 912 | at = carouselStep(b) | 914 | at = carouselStep(b) | ||
| 913 | } | 915 | } | ||
| 914 | w := sc.mon.Current() | 916 | w := sc.mon.Current() | ||
| 917 | + | n := len(w.Stash) | |||
| 915 | for _, p := range sc.mon.Layout() { | 918 | for _, p := range sc.mon.Layout() { | ||
| 916 | if p.Hidden { | 919 | if p.Hidden { | ||
| 917 | continue | 920 | continue | ||
| 918 | } | 921 | } | ||
| 919 | rm := sc.rects[p.ID] | 922 | rm := sc.rects[p.ID] | ||
| 920 | i := slices.IndexFunc(b.rects, func(r rectShot) bool { return r.id == p.ID }) | 923 | 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), w.wrapsCards()) { | 924 | + | if i >= 0 && at >= 0 && wrapped(w.stashIndex(p.ID), b.card, at, n, b.wraps) { |
| 922 | // The card went round the carousel to its other end: it | 925 | // The card went round the carousel to its other end: it | ||
| 923 | // fades in there instead of crossing the row. | 926 | // fades in there instead of crossing the row. | ||
| 924 | rm, i = rectMotion{}, -1 | 927 | rm, i = rectMotion{}, -1 | ||
| @@ -949,22 +952,23 @@ func (c *Core) transitionOverview(b *viewShot, now time.Time) { | |||||
| 949 | } | 952 | } | ||
| 950 | 953 | ||||
| 951 | // carouselStep is the selected card of the stash carousel b's screen shows | 954 | // carouselStep is the selected card of the stash carousel b's screen shows | ||
| 952 | - | // now, when it showed the same carousel in b too (the same workspace and | 955 | + | // now, when it showed the same carousel in b too (the same workspace, stash |
| 953 | - | // stash size); else -1. | 956 | + | // size and wrapping); else -1. |
| 954 | func carouselStep(b *viewShot) int { | 957 | func carouselStep(b *viewShot) int { | ||
| 955 | m := b.sc.mon | 958 | m := b.sc.mon | ||
| 956 | w := m.Current() | 959 | w := m.Current() | ||
| 957 | - | if b.card < 0 || w != b.ws || len(w.Stash) != b.stashLen { | 960 | + | if b.card < 0 || w != b.ws || len(w.Stash) != b.stashLen || w.wrapsCards() != b.wraps { |
| 958 | return -1 | 961 | return -1 | ||
| 959 | } | 962 | } | ||
| 960 | return m.cardAt(w) | 963 | return m.cardAt(w) | ||
| 961 | } | 964 | } | ||
| 962 | 965 | ||||
| 963 | // wrapped reports whether stash card i of a carousel of n cards went round | 966 | // 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 | 967 | + | // it, from one end to the other, when the selection went from card from to |
| 965 | - | // (carouselSlot) when the selection went from card from to card to. | 968 | + | // card to: it moved other than the cards that stayed on their side, which |
| 969 | + | // all shift by the old selection's move (carouselSlot). | |||
| 966 | func wrapped(i, from, to, n int, wraps bool) bool { | 970 | 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 | 971 | + | return i >= 0 && carouselSlot(i, to, n, wraps)-carouselSlot(i, from, n, wraps) != carouselSlot(from, to, n, wraps) |
| 968 | } | 972 | } | ||
| 969 | 973 | ||||
| 970 | // movedFrom is the shot of the other screen that showed the workspace b's | 974 | // movedFrom is the shot of the other screen that showed the workspace b's | ||
M internal/core/transition_stash_internal_test.go +50 -28
| @@ -474,35 +474,57 @@ func TestWithLeavingOrderIsDeterministic(t *testing.T) { | |||
| 474 | 474 | } | |
| 475 | 475 | } | |
| 476 | 476 | ||
| 477 | - | // Walking the stash carousel slides its cards one place; the card that goes | |
| 478 | - | // round to the other end fades in there instead of crossing the row. | |
| 477 | + | // Walking the stash carousel slides its cards; a card that goes round to | |
| 478 | + | // the other end fades in there instead of crossing the row, whatever the | |
| 479 | + | // direction, the count or the size of the step (a fast wheel). A carousel | |
| 480 | + | // that does not wrap round slides every card. | |
| 479 | 481 | func TestCarouselWrapFadesIn(t *testing.T) { | |
| 480 | - | c, ic, sc, _ := stashRig(t, true) | |
| 481 | - | m := sc.mon | |
| 482 | - | w := m.Current() | |
| 483 | - | w.Overflow = OverflowCascade | |
| 484 | - | c.applyAction(ActionToggleOverview) | |
| 485 | - | settleShown(t, c) | |
| 486 | - | m.selectCard(w, 0) | |
| 487 | - | settleShown(t, c) | |
| 488 | - | shots := c.snapshot(ic.now) | |
| 489 | - | m.OverviewMove(1, 0) | |
| 490 | - | c.transition(shots, ic.now) | |
| 491 | - | if m.cardAt(w) != 1 { | |
| 492 | - | t.Fatalf("setup: card %d", m.cardAt(w)) | |
| 493 | - | } | |
| 494 | - | // Three cards: the selection moved from 0 to 1, card 2 went round | |
| 495 | - | // from the right end to the left one. | |
| 496 | - | for i, e := range w.Stash { | |
| 497 | - | rm := sc.rects[e.ID] | |
| 498 | - | if i == 2 { | |
| 499 | - | if rm.dx != 0 || !rm.fade.on { | |
| 500 | - | t.Fatalf("wrapped card: dx %v, fade %v", rm.dx, rm.fade.on) | |
| 482 | + | for _, tc := range []struct { | |
| 483 | + | name string | |
| 484 | + | n, from, to int | |
| 485 | + | wrapped []int | |
| 486 | + | }{ | |
| 487 | + | {"right", 3, 0, 1, []int{2}}, | |
| 488 | + | {"left", 3, 0, 2, []int{1}}, | |
| 489 | + | {"even", 4, 3, 0, []int{1}}, | |
| 490 | + | {"jump", 5, 0, 2, []int{3, 4}}, | |
| 491 | + | {"two cards", 2, 0, 1, nil}, | |
| 492 | + | } { | |
| 493 | + | t.Run(tc.name, func(t *testing.T) { | |
| 494 | + | c, ic, _ := fadeCore(t) | |
| 495 | + | c.cfg.Animations.On = true | |
| 496 | + | sc := c.cur() | |
| 497 | + | m := sc.mon | |
| 498 | + | w := m.Current() | |
| 499 | + | w.Overflow = OverflowCascade | |
| 500 | + | m.AddWindow(1) | |
| 501 | + | for id := WindowID(2); id < WindowID(2+tc.n); id++ { | |
| 502 | + | m.AddWindow(id) | |
| 503 | + | w.FocusID(id) | |
| 504 | + | c.applyAction(ActionToggleWindowStash) | |
| 501 | 505 | } | |
| 502 | - | continue | |
| 503 | - | } | |
| 504 | - | if rm.dx == 0 || rm.fade.on { | |
| 505 | - | t.Fatalf("card %d: dx %v, fade %v", i, rm.dx, rm.fade.on) | |
| 506 | - | } | |
| 506 | + | w.FocusID(1) | |
| 507 | + | c.applyAction(ActionToggleOverview) | |
| 508 | + | m.selectCard(w, tc.from) | |
| 509 | + | settleShown(t, c) | |
| 510 | + | shots := c.snapshot(ic.now) | |
| 511 | + | m.selectCard(w, tc.to) | |
| 512 | + | c.transition(shots, ic.now) | |
| 513 | + | for i, e := range w.Stash { | |
| 514 | + | if previewOf(t, m.Layout(), e.ID).Hidden { | |
| 515 | + | continue | |
| 516 | + | } | |
| 517 | + | rm := sc.rects[e.ID] | |
| 518 | + | if slices.Contains(tc.wrapped, i) { | |
| 519 | + | if rm.dx != 0 || !rm.fade.on { | |
| 520 | + | t.Fatalf("wrapped card %d: dx %v, fade %v", i, rm.dx, rm.fade.on) | |
| 521 | + | } | |
| 522 | + | continue | |
| 523 | + | } | |
| 524 | + | if rm.dx == 0 || rm.fade.on { | |
| 525 | + | t.Fatalf("card %d: dx %v, fade %v", i, rm.dx, rm.fade.on) | |
| 526 | + | } | |
| 527 | + | } | |
| 528 | + | }) | |
| 507 | 529 | } | |
| 508 | 530 | } | |
| @@ -474,35 +474,57 @@ func TestWithLeavingOrderIsDeterministic(t *testing.T) { | |||||
| 474 | } | 474 | } | ||
| 475 | } | 475 | } | ||
| 476 | 476 | ||||
| 477 | - | // Walking the stash carousel slides its cards one place; the card that goes | 477 | + | // Walking the stash carousel slides its cards; a card that goes round to |
| 478 | - | // round to the other end fades in there instead of crossing the row. | 478 | + | // the other end fades in there instead of crossing the row, whatever the |
| 479 | + | // direction, the count or the size of the step (a fast wheel). A carousel | |||
| 480 | + | // that does not wrap round slides every card. | |||
| 479 | func TestCarouselWrapFadesIn(t *testing.T) { | 481 | func TestCarouselWrapFadesIn(t *testing.T) { | ||
| 480 | - | c, ic, sc, _ := stashRig(t, true) | 482 | + | for _, tc := range []struct { |
| 481 | - | m := sc.mon | 483 | + | name string |
| 482 | - | w := m.Current() | 484 | + | n, from, to int |
| 483 | - | w.Overflow = OverflowCascade | 485 | + | wrapped []int |
| 484 | - | c.applyAction(ActionToggleOverview) | 486 | + | }{ |
| 485 | - | settleShown(t, c) | 487 | + | {"right", 3, 0, 1, []int{2}}, |
| 486 | - | m.selectCard(w, 0) | 488 | + | {"left", 3, 0, 2, []int{1}}, |
| 487 | - | settleShown(t, c) | 489 | + | {"even", 4, 3, 0, []int{1}}, |
| 488 | - | shots := c.snapshot(ic.now) | 490 | + | {"jump", 5, 0, 2, []int{3, 4}}, |
| 489 | - | m.OverviewMove(1, 0) | 491 | + | {"two cards", 2, 0, 1, nil}, |
| 490 | - | c.transition(shots, ic.now) | 492 | + | } { |
| 491 | - | if m.cardAt(w) != 1 { | 493 | + | t.Run(tc.name, func(t *testing.T) { |
| 492 | - | t.Fatalf("setup: card %d", m.cardAt(w)) | 494 | + | c, ic, _ := fadeCore(t) |
| 493 | - | } | 495 | + | c.cfg.Animations.On = true |
| 494 | - | // Three cards: the selection moved from 0 to 1, card 2 went round | 496 | + | sc := c.cur() |
| 495 | - | // from the right end to the left one. | 497 | + | m := sc.mon |
| 496 | - | for i, e := range w.Stash { | 498 | + | w := m.Current() |
| 497 | - | rm := sc.rects[e.ID] | 499 | + | w.Overflow = OverflowCascade |
| 498 | - | if i == 2 { | 500 | + | m.AddWindow(1) |
| 499 | - | if rm.dx != 0 || !rm.fade.on { | 501 | + | for id := WindowID(2); id < WindowID(2+tc.n); id++ { |
| 500 | - | t.Fatalf("wrapped card: dx %v, fade %v", rm.dx, rm.fade.on) | 502 | + | m.AddWindow(id) |
| 503 | + | w.FocusID(id) | |||
| 504 | + | c.applyAction(ActionToggleWindowStash) | |||
| 501 | } | 505 | } | ||
| 502 | - | continue | 506 | + | w.FocusID(1) |
| 503 | - | } | 507 | + | c.applyAction(ActionToggleOverview) |
| 504 | - | if rm.dx == 0 || rm.fade.on { | 508 | + | m.selectCard(w, tc.from) |
| 505 | - | t.Fatalf("card %d: dx %v, fade %v", i, rm.dx, rm.fade.on) | 509 | + | settleShown(t, c) |
| 506 | - | } | 510 | + | shots := c.snapshot(ic.now) |
| 511 | + | m.selectCard(w, tc.to) | |||
| 512 | + | c.transition(shots, ic.now) | |||
| 513 | + | for i, e := range w.Stash { | |||
| 514 | + | if previewOf(t, m.Layout(), e.ID).Hidden { | |||
| 515 | + | continue | |||
| 516 | + | } | |||
| 517 | + | rm := sc.rects[e.ID] | |||
| 518 | + | if slices.Contains(tc.wrapped, i) { | |||
| 519 | + | if rm.dx != 0 || !rm.fade.on { | |||
| 520 | + | t.Fatalf("wrapped card %d: dx %v, fade %v", i, rm.dx, rm.fade.on) | |||
| 521 | + | } | |||
| 522 | + | continue | |||
| 523 | + | } | |||
| 524 | + | if rm.dx == 0 || rm.fade.on { | |||
| 525 | + | t.Fatalf("card %d: dx %v, fade %v", i, rm.dx, rm.fade.on) | |||
| 526 | + | } | |||
| 527 | + | } | |||
| 528 | + | }) | |||
| 507 | } | 529 | } | ||
| 508 | } | 530 | } | ||