Merge pull request 'feat(overview): unfold the stash as a carousel centred on the selection' (#58) from feat/stash-carousel into main
Reviewed-on: https://git.bnema.dev/bnema/neferwl/pulls/58
8 files · +500 -134
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 row under the workspace with `layout.overflow = cascade`, a column on its left otherwise. 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. Moving to another workspace selects its windows rather than a carousel that wraps round, even when its stash has the focus, so browsing workspaces never stops on one. | |
| 280 | 280 | ||
| 281 | - | With `layout.overflow = cascade`, workspaces sit side by side, each band of columns is a lane, and the stash sits under the lanes. Down (or fingers down on the touchpad, with natural scroll off) moves to the lane below; past the last lane it unfolds the stash. Left/right walk the cards, then 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 row under the workspace with `layout.overflow = cascade`, a column on its left otherwise. 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. Moving to another workspace selects its windows rather than a carousel that wraps round, even when its stash has the focus, so browsing workspaces never stops on one. |
| 280 | 280 | ||||
| 281 | - | With `layout.overflow = cascade`, workspaces sit side by side, each band of columns is a lane, and the stash sits under the lanes. Down (or fingers down on the touchpad, with natural scroll off) moves to the lane below; past the last lane it unfolds the stash. Left/right walk the cards, then 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 +117 -45
| @@ -57,8 +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 into a strip of every card (unfold), the | |
| 61 | - | // selection framed, and the moves along that axis walk it. 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 | |
| 62 | 62 | // overview pins the pile to the left edge of its row, which takes the width | |
| 63 | 63 | // the pile leaves. A horizontal one centres it on its row, in a band | |
| 64 | 64 | // reserved under the selected lane (pileBand): the lane and the band are | |
| @@ -402,11 +402,12 @@ 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 unfolded stash and the stack cards before changing workspace | |
| 406 | - | // (overviewVertical). A side-by-side one always changes workspace, since | |
| 407 | - | // 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 | - | if m.overviewAxis() == verticalAxis { | |
| 410 | + | if m.overviewAxis() == verticalAxis && m.cardAt(m.Current()) < 0 { | |
| 410 | 411 | m.overviewVertical(d) | |
| 411 | 412 | return | |
| 412 | 413 | } | |
| @@ -414,17 +415,43 @@ 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, stopping at either end. It reports whether a card was | |
| 418 | - | // selected. | |
| 418 | + | // the unfolded pile, wrapping round (wrapsCards), else staying at either | |
| 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 | - | m.selectCard(w, min(max(at+d, 0), len(w.Stash)-1)) | |
| 425 | + | m.stepCard(w, at, d) | |
| 425 | 426 | return true | |
| 426 | 427 | } | |
| 427 | 428 | ||
| 429 | + | // stepCard selects the stash card d entries from card at of w, wrapping | |
| 430 | + | // round when the carousel does (wrapsCards). It reports false, selecting | |
| 431 | + | // nothing, past an end of one that does not. | |
| 432 | + | func (m *Monitor) stepCard(w *Workspace, at, d int) bool { | |
| 433 | + | n, next := len(w.Stash), at+d | |
| 434 | + | if next < 0 || next >= n { | |
| 435 | + | if !w.wrapsCards() { | |
| 436 | + | return false | |
| 437 | + | } | |
| 438 | + | next = (next%n + n) % n | |
| 439 | + | } | |
| 440 | + | m.selectCard(w, next) | |
| 441 | + | return true | |
| 442 | + | } | |
| 443 | + | ||
| 444 | + | // wrapsCards reports whether the unfolded stash of w wraps round, drawn | |
| 445 | + | // (unfold) and walked (stepCard). A carousel of fewer than three cards does | |
| 446 | + | // not, so that each side has the card its move selects; nor does the one | |
| 447 | + | // of a stash-only workspace, which has nothing beside its pile to leave it | |
| 448 | + | // for. Past the ends of one that does not, a move goes on as from a lane | |
| 449 | + | // (cascade) or stops (a vertical overview). | |
| 450 | + | func (w *Workspace) wrapsCards() bool { | |
| 451 | + | _, k := w.stackHead() | |
| 452 | + | return k > 0 && len(w.Stash) >= 3 | |
| 453 | + | } | |
| 454 | + | ||
| 428 | 455 | // overviewLane is the vertical move of an overview whose workspaces sit side | |
| 429 | 456 | // by side (cascade). From the unfolded stash, up leaves it for the last lane | |
| 430 | 457 | // (leavePile) and down stays: the stash is the bottom. Else the selection | |
| @@ -480,18 +507,16 @@ func (m *Monitor) selectLaneColumn(w *Workspace, i int) { | |||
| 480 | 507 | ||
| 481 | 508 | // overviewAcross is the horizontal move of an overview whose workspaces sit | |
| 482 | 509 | // side by side (cascade): a column inside the selected lane, or a card of | |
| 483 | - | // the unfolded stash. Past the first column or card it goes to the previous | |
| 484 | - | // workspace; past the last, to the next workspace. Covering floats fan | |
| 485 | - | // along this axis and are traversed before a workspace: the card on the | |
| 486 | - | // left is the one behind. | |
| 510 | + | // the unfolded stash, which wraps round (stepCard). Past the first column | |
| 511 | + | // or a card that does not wrap, it goes to the previous workspace; past | |
| 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. | |
| 487 | 514 | func (m *Monitor) overviewAcross(dx int) { | |
| 488 | 515 | w := m.Current() | |
| 489 | 516 | if at := m.cardAt(w); at >= 0 { | |
| 490 | - | if next := at + dx; next >= 0 && next < len(w.Stash) { | |
| 491 | - | m.selectCard(w, next) | |
| 492 | - | return | |
| 517 | + | if !m.stepCard(w, at, dx) { | |
| 518 | + | m.overviewStep(dx) | |
| 493 | 519 | } | |
| 494 | - | m.overviewStep(dx) | |
| 495 | 520 | return | |
| 496 | 521 | } | |
| 497 | 522 | if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 { | |
| @@ -529,12 +554,17 @@ func (m *Monitor) overviewVertical(dy int) { | |||
| 529 | 554 | } | |
| 530 | 555 | ||
| 531 | 556 | // overviewStep shows the workspace d rows from the current one; it stays | |
| 532 | - | // on the first or last row. | |
| 557 | + | // on the first or last row. The row selects its stack even when its stash | |
| 558 | + | // has the focus, if the stash wraps round (wrapsCards): the moves that | |
| 559 | + | // stepped there would only walk the carousel, never reaching the next row. | |
| 533 | 560 | func (m *Monitor) overviewStep(d int) { | |
| 534 | 561 | rows := m.overviewWorkspaces() | |
| 535 | 562 | i := indexOf(rows, m.Current()) + d | |
| 536 | 563 | if i >= 0 && i < len(rows) { | |
| 537 | 564 | m.showOverview(rows[i]) | |
| 565 | + | if w := m.Current(); m.cardAt(w) >= 0 && w.wrapsCards() { | |
| 566 | + | m.clearCard() | |
| 567 | + | } | |
| 538 | 568 | } | |
| 539 | 569 | } | |
| 540 | 570 | ||
| @@ -916,48 +946,90 @@ func (w *Workspace) pile(dst []Placement, axis layoutAxis, slot Rect, dim bool, | |||
| 916 | 946 | return dst | |
| 917 | 947 | } | |
| 918 | 948 | ||
| 919 | - | // unfold lays the stash of w out as a strip of every card along axis, from | |
| 920 | - | // the folded pile's card rect: a row in a horizontal overview, a column in | |
| 921 | - | // a vertical one. The strip is centred on the pile and kept in the overview | |
| 922 | - | // area. Longer than the area, it scrolls like a wide row: the selected card | |
| 923 | - | // at is centred, the strip stopping at the area's edges. The other cards | |
| 924 | - | // are dimmed; those out of the area are hidden. The cards are appended to | |
| 925 | - | // dst. | |
| 949 | + | // unfold lays the stash of w out from the folded pile's card rect as a | |
| 950 | + | // carousel along axis: a row in a horizontal overview, a column in a | |
| 951 | + | // vertical one. The selected card at sits on the pile, in the middle; the | |
| 952 | + | // cards before it come on the left (above), the ones after it on the right | |
| 953 | + | // (below), in stash order, wrapping round (wrapsCards) with the oldest | |
| 954 | + | // following the newest (carouselSlot). A wider gap marks that seam. A | |
| 955 | + | // carousel that fits the overview area is shifted into it. Longer, one | |
| 956 | + | // that wraps keeps the selection in the middle; one that does not stops at | |
| 957 | + | // the area's edges, like a wide row. The cards past the area are hidden, | |
| 958 | + | // the ones other than at dimmed. The cards are appended to dst. | |
| 926 | 959 | func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int) []Placement { | |
| 927 | 960 | n := len(w.Stash) | |
| 928 | 961 | u := w.overviewArea() | |
| 929 | - | size, origin, lo, hi := folded.W, folded.X, u.X, u.X+u.W | |
| 962 | + | size, origin, lo, room := folded.W, folded.X, u.X, u.W | |
| 930 | 963 | if axis == verticalAxis { | |
| 931 | - | size, origin, lo, hi = folded.H, folded.Y, u.Y, u.Y+u.H | |
| 964 | + | size, origin, lo, room = folded.H, folded.Y, u.Y, u.H | |
| 932 | 965 | } | |
| 933 | 966 | stride := size + 2*w.cardStep() | |
| 934 | - | total := n*stride - (stride - size) | |
| 935 | - | start := origin + size/2 - total/2 | |
| 936 | - | if total > hi-lo { | |
| 937 | - | start = origin + size/2 - at*stride - size/2 | |
| 938 | - | start = min(max(start, hi-total), lo) | |
| 939 | - | } else { | |
| 940 | - | start = min(max(start, lo), hi-total) | |
| 967 | + | // The extra room at the seam, on top of the stride. | |
| 968 | + | gap := size / 4 | |
| 969 | + | wraps := w.wrapsCards() | |
| 970 | + | pos := func(s int) int { | |
| 971 | + | p := origin + s*stride | |
| 972 | + | switch { | |
| 973 | + | case s > 0 && at+s >= n: | |
| 974 | + | p += gap | |
| 975 | + | case s < 0 && at+s < 0: | |
| 976 | + | p -= gap | |
| 977 | + | } | |
| 978 | + | return p | |
| 979 | + | } | |
| 980 | + | // The slots of the ends. | |
| 981 | + | from, to := -at, n-1-at | |
| 982 | + | if wraps { | |
| 983 | + | from, to = -(n / 2), (n-1)/2 | |
| 984 | + | } | |
| 985 | + | first, total := pos(from), pos(to)+size-pos(from) | |
| 986 | + | var shift int | |
| 987 | + | switch { | |
| 988 | + | case total <= room: | |
| 989 | + | shift = min(max(first, lo), lo+room-total) - first | |
| 990 | + | case !wraps: | |
| 991 | + | shift = min(max(first, lo+room-total), lo) - first | |
| 941 | 992 | } | |
| 942 | 993 | for i := range w.Stash { | |
| 943 | 994 | r := folded | |
| 944 | - | if axis == verticalAxis { | |
| 945 | - | r.Y = start + i*stride | |
| 995 | + | if p := pos(carouselSlot(i, at, n, wraps)) + shift; axis == verticalAxis { | |
| 996 | + | r.Y = p | |
| 946 | 997 | } else { | |
| 947 | - | r.X = start + i*stride | |
| 948 | - | } | |
| 949 | - | p := Placement{ID: w.Stash[i].ID, Rect: r, Preview: overviewCardZoom, Card: true, Focused: i == at} | |
| 950 | - | if i != at { | |
| 951 | - | p.peeking(1) | |
| 998 | + | r.X = p | |
| 952 | 999 | } | |
| 953 | - | if !r.Overlaps(u) { | |
| 954 | - | p.Hidden, p.Preview, p.Focused = true, 0, false | |
| 955 | - | } | |
| 956 | - | dst = append(dst, p) | |
| 1000 | + | dst = w.unfoldedCard(dst, i, r, at, u) | |
| 957 | 1001 | } | |
| 958 | 1002 | return dst | |
| 959 | 1003 | } | |
| 960 | 1004 | ||
| 1005 | + | // carouselSlot is the place of stash card i in a carousel of n cards | |
| 1006 | + | // centred on card at: 0 for at, negative on the left (above), positive on | |
| 1007 | + | // the right (below). Wrapping round, the left side takes the extra card of | |
| 1008 | + | // an even count; else the cards keep the stash order. | |
| 1009 | + | func carouselSlot(i, at, n int, wraps bool) int { | |
| 1010 | + | if !wraps { | |
| 1011 | + | return i - at | |
| 1012 | + | } | |
| 1013 | + | s := (i - at + n) % n | |
| 1014 | + | if s > (n-1)/2 { | |
| 1015 | + | s -= n | |
| 1016 | + | } | |
| 1017 | + | return s | |
| 1018 | + | } | |
| 1019 | + | ||
| 1020 | + | // unfoldedCard appends stash card i of an unfolded pile at r to dst: framed | |
| 1021 | + | // when it is the selected one at, dimmed otherwise, hidden out of the area u. | |
| 1022 | + | func (w *Workspace) unfoldedCard(dst []Placement, i int, r Rect, at int, u Rect) []Placement { | |
| 1023 | + | p := Placement{ID: w.Stash[i].ID, Rect: r, Preview: overviewCardZoom, Card: true, Focused: i == at} | |
| 1024 | + | if i != at { | |
| 1025 | + | p.peeking(1) | |
| 1026 | + | } | |
| 1027 | + | if !r.Overlaps(u) { | |
| 1028 | + | p.Hidden, p.Preview, p.Focused = true, 0, false | |
| 1029 | + | } | |
| 1030 | + | return append(dst, p) | |
| 1031 | + | } | |
| 1032 | + | ||
| 961 | 1033 | // overviewZoom is the scale that fits every column of w in the usable | |
| 962 | 1034 | // width, within the overview bounds, in an overview along axis: only a | |
| 963 | 1035 | // vertical one sets the stash pile beside the row. | |
| @@ -57,8 +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 into a strip of every card (unfold), the | 60 | + | // unfolds along the overview axis into a carousel centred on the selection |
| 61 | - | // selection framed, and the moves along that axis walk it. A vertical | 61 | + | // (unfold), framed, and the moves along that axis walk it round. A vertical |
| 62 | // 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 | ||
| 63 | // 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 | ||
| 64 | // 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 | ||
| @@ -402,11 +402,12 @@ 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 unfolded stash and the stack cards before changing workspace | 405 | + | // walks the stack cards before changing workspace (overviewVertical); from |
| 406 | - | // (overviewVertical). A side-by-side one always changes workspace, since | 406 | + | // a selected stash card, which the move keys walk, it changes workspace |
| 407 | - | // 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 { | 410 | + | if m.overviewAxis() == verticalAxis && m.cardAt(m.Current()) < 0 { |
| 410 | m.overviewVertical(d) | 411 | m.overviewVertical(d) | ||
| 411 | return | 412 | return | ||
| 412 | } | 413 | } | ||
| @@ -414,17 +415,43 @@ 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, stopping at either end. It reports whether a card was | 418 | + | // the unfolded pile, wrapping round (wrapsCards), else staying at either |
| 418 | - | // 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 | - | m.selectCard(w, min(max(at+d, 0), len(w.Stash)-1)) | 425 | + | m.stepCard(w, at, d) |
| 425 | return true | 426 | return true | ||
| 426 | } | 427 | } | ||
| 427 | 428 | ||||
| 429 | + | // stepCard selects the stash card d entries from card at of w, wrapping | |||
| 430 | + | // round when the carousel does (wrapsCards). It reports false, selecting | |||
| 431 | + | // nothing, past an end of one that does not. | |||
| 432 | + | func (m *Monitor) stepCard(w *Workspace, at, d int) bool { | |||
| 433 | + | n, next := len(w.Stash), at+d | |||
| 434 | + | if next < 0 || next >= n { | |||
| 435 | + | if !w.wrapsCards() { | |||
| 436 | + | return false | |||
| 437 | + | } | |||
| 438 | + | next = (next%n + n) % n | |||
| 439 | + | } | |||
| 440 | + | m.selectCard(w, next) | |||
| 441 | + | return true | |||
| 442 | + | } | |||
| 443 | + | ||||
| 444 | + | // wrapsCards reports whether the unfolded stash of w wraps round, drawn | |||
| 445 | + | // (unfold) and walked (stepCard). A carousel of fewer than three cards does | |||
| 446 | + | // not, so that each side has the card its move selects; nor does the one | |||
| 447 | + | // of a stash-only workspace, which has nothing beside its pile to leave it | |||
| 448 | + | // for. Past the ends of one that does not, a move goes on as from a lane | |||
| 449 | + | // (cascade) or stops (a vertical overview). | |||
| 450 | + | func (w *Workspace) wrapsCards() bool { | |||
| 451 | + | _, k := w.stackHead() | |||
| 452 | + | return k > 0 && len(w.Stash) >= 3 | |||
| 453 | + | } | |||
| 454 | + | ||||
| 428 | // overviewLane is the vertical move of an overview whose workspaces sit side | 455 | // overviewLane is the vertical move of an overview whose workspaces sit side | ||
| 429 | // by side (cascade). From the unfolded stash, up leaves it for the last lane | 456 | // by side (cascade). From the unfolded stash, up leaves it for the last lane | ||
| 430 | // (leavePile) and down stays: the stash is the bottom. Else the selection | 457 | // (leavePile) and down stays: the stash is the bottom. Else the selection | ||
| @@ -480,18 +507,16 @@ func (m *Monitor) selectLaneColumn(w *Workspace, i int) { | |||||
| 480 | 507 | ||||
| 481 | // overviewAcross is the horizontal move of an overview whose workspaces sit | 508 | // overviewAcross is the horizontal move of an overview whose workspaces sit | ||
| 482 | // 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 | ||
| 483 | - | // the unfolded stash. Past the first column or card it goes to the previous | 510 | + | // the unfolded stash, which wraps round (stepCard). Past the first column |
| 484 | - | // workspace; past the last, to the next workspace. Covering floats fan | 511 | + | // or a card that does not wrap, it goes to the previous workspace; past |
| 485 | - | // along this axis and are traversed before a workspace: the card on the | 512 | + | // the last, to the next workspace. Covering floats fan along this axis and |
| 486 | - | // left is the one behind. | 513 | + | // are traversed before a workspace: the card on the left is the one behind. |
| 487 | func (m *Monitor) overviewAcross(dx int) { | 514 | func (m *Monitor) overviewAcross(dx int) { | ||
| 488 | w := m.Current() | 515 | w := m.Current() | ||
| 489 | if at := m.cardAt(w); at >= 0 { | 516 | if at := m.cardAt(w); at >= 0 { | ||
| 490 | - | if next := at + dx; next >= 0 && next < len(w.Stash) { | 517 | + | if !m.stepCard(w, at, dx) { |
| 491 | - | m.selectCard(w, next) | 518 | + | m.overviewStep(dx) |
| 492 | - | return | |||
| 493 | } | 519 | } | ||
| 494 | - | m.overviewStep(dx) | |||
| 495 | return | 520 | return | ||
| 496 | } | 521 | } | ||
| 497 | if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 { | 522 | if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 { | ||
| @@ -529,12 +554,17 @@ func (m *Monitor) overviewVertical(dy int) { | |||||
| 529 | } | 554 | } | ||
| 530 | 555 | ||||
| 531 | // overviewStep shows the workspace d rows from the current one; it stays | 556 | // overviewStep shows the workspace d rows from the current one; it stays | ||
| 532 | - | // on the first or last row. | 557 | + | // on the first or last row. The row selects its stack even when its stash |
| 558 | + | // has the focus, if the stash wraps round (wrapsCards): the moves that | |||
| 559 | + | // stepped there would only walk the carousel, never reaching the next row. | |||
| 533 | func (m *Monitor) overviewStep(d int) { | 560 | func (m *Monitor) overviewStep(d int) { | ||
| 534 | rows := m.overviewWorkspaces() | 561 | rows := m.overviewWorkspaces() | ||
| 535 | i := indexOf(rows, m.Current()) + d | 562 | i := indexOf(rows, m.Current()) + d | ||
| 536 | if i >= 0 && i < len(rows) { | 563 | if i >= 0 && i < len(rows) { | ||
| 537 | m.showOverview(rows[i]) | 564 | m.showOverview(rows[i]) | ||
| 565 | + | if w := m.Current(); m.cardAt(w) >= 0 && w.wrapsCards() { | |||
| 566 | + | m.clearCard() | |||
| 567 | + | } | |||
| 538 | } | 568 | } | ||
| 539 | } | 569 | } | ||
| 540 | 570 | ||||
| @@ -916,48 +946,90 @@ func (w *Workspace) pile(dst []Placement, axis layoutAxis, slot Rect, dim bool, | |||||
| 916 | return dst | 946 | return dst | ||
| 917 | } | 947 | } | ||
| 918 | 948 | ||||
| 919 | - | // unfold lays the stash of w out as a strip of every card along axis, from | 949 | + | // unfold lays the stash of w out from the folded pile's card rect as a |
| 920 | - | // the folded pile's card rect: a row in a horizontal overview, a column in | 950 | + | // carousel along axis: a row in a horizontal overview, a column in a |
| 921 | - | // a vertical one. The strip is centred on the pile and kept in the overview | 951 | + | // vertical one. The selected card at sits on the pile, in the middle; the |
| 922 | - | // area. Longer than the area, it scrolls like a wide row: the selected card | 952 | + | // cards before it come on the left (above), the ones after it on the right |
| 923 | - | // at is centred, the strip stopping at the area's edges. The other cards | 953 | + | // (below), in stash order, wrapping round (wrapsCards) with the oldest |
| 924 | - | // are dimmed; those out of the area are hidden. The cards are appended to | 954 | + | // following the newest (carouselSlot). A wider gap marks that seam. A |
| 925 | - | // dst. | 955 | + | // carousel that fits the overview area is shifted into it. Longer, one |
| 956 | + | // that wraps keeps the selection in the middle; one that does not stops at | |||
| 957 | + | // the area's edges, like a wide row. The cards past the area are hidden, | |||
| 958 | + | // the ones other than at dimmed. The cards are appended to dst. | |||
| 926 | func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int) []Placement { | 959 | func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int) []Placement { | ||
| 927 | n := len(w.Stash) | 960 | n := len(w.Stash) | ||
| 928 | u := w.overviewArea() | 961 | u := w.overviewArea() | ||
| 929 | - | size, origin, lo, hi := folded.W, folded.X, u.X, u.X+u.W | 962 | + | size, origin, lo, room := folded.W, folded.X, u.X, u.W |
| 930 | if axis == verticalAxis { | 963 | if axis == verticalAxis { | ||
| 931 | - | size, origin, lo, hi = folded.H, folded.Y, u.Y, u.Y+u.H | 964 | + | size, origin, lo, room = folded.H, folded.Y, u.Y, u.H |
| 932 | } | 965 | } | ||
| 933 | stride := size + 2*w.cardStep() | 966 | stride := size + 2*w.cardStep() | ||
| 934 | - | total := n*stride - (stride - size) | 967 | + | // The extra room at the seam, on top of the stride. |
| 935 | - | start := origin + size/2 - total/2 | 968 | + | gap := size / 4 |
| 936 | - | if total > hi-lo { | 969 | + | wraps := w.wrapsCards() |
| 937 | - | start = origin + size/2 - at*stride - size/2 | 970 | + | pos := func(s int) int { |
| 938 | - | start = min(max(start, hi-total), lo) | 971 | + | p := origin + s*stride |
| 939 | - | } else { | 972 | + | switch { |
| 940 | - | start = min(max(start, lo), hi-total) | 973 | + | case s > 0 && at+s >= n: |
| 974 | + | p += gap | |||
| 975 | + | case s < 0 && at+s < 0: | |||
| 976 | + | p -= gap | |||
| 977 | + | } | |||
| 978 | + | return p | |||
| 979 | + | } | |||
| 980 | + | // The slots of the ends. | |||
| 981 | + | from, to := -at, n-1-at | |||
| 982 | + | if wraps { | |||
| 983 | + | from, to = -(n / 2), (n-1)/2 | |||
| 984 | + | } | |||
| 985 | + | first, total := pos(from), pos(to)+size-pos(from) | |||
| 986 | + | var shift int | |||
| 987 | + | switch { | |||
| 988 | + | case total <= room: | |||
| 989 | + | shift = min(max(first, lo), lo+room-total) - first | |||
| 990 | + | case !wraps: | |||
| 991 | + | shift = min(max(first, lo+room-total), lo) - first | |||
| 941 | } | 992 | } | ||
| 942 | for i := range w.Stash { | 993 | for i := range w.Stash { | ||
| 943 | r := folded | 994 | r := folded | ||
| 944 | - | if axis == verticalAxis { | 995 | + | if p := pos(carouselSlot(i, at, n, wraps)) + shift; axis == verticalAxis { |
| 945 | - | r.Y = start + i*stride | 996 | + | r.Y = p |
| 946 | } else { | 997 | } else { | ||
| 947 | - | r.X = start + i*stride | 998 | + | r.X = p |
| 948 | - | } | |||
| 949 | - | p := Placement{ID: w.Stash[i].ID, Rect: r, Preview: overviewCardZoom, Card: true, Focused: i == at} | |||
| 950 | - | if i != at { | |||
| 951 | - | p.peeking(1) | |||
| 952 | } | 999 | } | ||
| 953 | - | if !r.Overlaps(u) { | 1000 | + | dst = w.unfoldedCard(dst, i, r, at, u) |
| 954 | - | p.Hidden, p.Preview, p.Focused = true, 0, false | |||
| 955 | - | } | |||
| 956 | - | dst = append(dst, p) | |||
| 957 | } | 1001 | } | ||
| 958 | return dst | 1002 | return dst | ||
| 959 | } | 1003 | } | ||
| 960 | 1004 | ||||
| 1005 | + | // carouselSlot is the place of stash card i in a carousel of n cards | |||
| 1006 | + | // centred on card at: 0 for at, negative on the left (above), positive on | |||
| 1007 | + | // the right (below). Wrapping round, the left side takes the extra card of | |||
| 1008 | + | // an even count; else the cards keep the stash order. | |||
| 1009 | + | func carouselSlot(i, at, n int, wraps bool) int { | |||
| 1010 | + | if !wraps { | |||
| 1011 | + | return i - at | |||
| 1012 | + | } | |||
| 1013 | + | s := (i - at + n) % n | |||
| 1014 | + | if s > (n-1)/2 { | |||
| 1015 | + | s -= n | |||
| 1016 | + | } | |||
| 1017 | + | return s | |||
| 1018 | + | } | |||
| 1019 | + | ||||
| 1020 | + | // unfoldedCard appends stash card i of an unfolded pile at r to dst: framed | |||
| 1021 | + | // when it is the selected one at, dimmed otherwise, hidden out of the area u. | |||
| 1022 | + | func (w *Workspace) unfoldedCard(dst []Placement, i int, r Rect, at int, u Rect) []Placement { | |||
| 1023 | + | p := Placement{ID: w.Stash[i].ID, Rect: r, Preview: overviewCardZoom, Card: true, Focused: i == at} | |||
| 1024 | + | if i != at { | |||
| 1025 | + | p.peeking(1) | |||
| 1026 | + | } | |||
| 1027 | + | if !r.Overlaps(u) { | |||
| 1028 | + | p.Hidden, p.Preview, p.Focused = true, 0, false | |||
| 1029 | + | } | |||
| 1030 | + | return append(dst, p) | |||
| 1031 | + | } | |||
| 1032 | + | ||||
| 961 | // overviewZoom is the scale that fits every column of w in the usable | 1033 | // overviewZoom is the scale that fits every column of w in the usable | ||
| 962 | // width, within the overview bounds, in an overview along axis: only a | 1034 | // width, within the overview bounds, in an overview along axis: only a | ||
| 963 | // vertical one sets the stash pile beside the row. | 1035 | // vertical one sets the stash pile beside the row. | ||
M internal/core/overview_cascade_test.go +7 -7
| @@ -344,7 +344,8 @@ func TestOverviewCascadeKeys(t *testing.T) { | |||
| 344 | 344 | // With a stash: left from the first column still goes to the | |
| 345 | 345 | // previous workspace. Down past the last band enters the | |
| 346 | 346 | // stash, up comes back to the band, and left past the first | |
| 347 | - | // card goes to the previous workspace. | |
| 347 | + | // card goes to the previous workspace: two cards do not | |
| 348 | + | // wrap round. | |
| 348 | 349 | withStash(m, 2) | |
| 349 | 350 | w.FocusID(7) | |
| 350 | 351 | m.ToggleOverview() | |
| @@ -473,10 +474,9 @@ func TestOverviewCascadeStashPile(t *testing.T) { | |||
| 473 | 474 | c.swipeUpdate(ports.SwipeUpdate{DX: 40}) | |
| 474 | 475 | } | |
| 475 | 476 | c.swipeEnd(ports.SwipeEnd{}) | |
| 476 | - | // A horizontal three-finger swipe is a workspace step, even over a | |
| 477 | - | // stash card. | |
| 478 | - | if m.Current() != m.Workspaces[1] { | |
| 479 | - | t.Fatalf("horizontal swipe over a stash card: workspace %d", indexOf(m.Workspaces, m.Current())) | |
| 477 | + | // A horizontal three-finger swipe walks the carousel, wrapping round. | |
| 478 | + | if want := (at + 1) % len(w.Stash); m.Current() != w || m.cardAt(w) != want { | |
| 479 | + | t.Fatalf("horizontal swipe over a stash card: workspace %d, card %d, want %d", indexOf(m.Workspaces, m.Current()), m.cardAt(w), want) | |
| 480 | 480 | } | |
| 481 | 481 | if got := stashIDs(w); !slices.Equal(got, before) { | |
| 482 | 482 | t.Fatal("stash reordered") | |
| @@ -930,8 +930,8 @@ func TestOverviewFingerLatchSurvivesAxisChange(t *testing.T) { | |||
| 930 | 930 | } | |
| 931 | 931 | } | |
| 932 | 932 | ||
| 933 | - | // A stash-only cascade workspace has no columns to select: right from its | |
| 934 | - | // pile goes straight to the next workspace. | |
| 933 | + | // A stash-only cascade workspace has nothing above its pile: right from its | |
| 934 | + | // pile goes straight to the next workspace, left to the previous one. | |
| 935 | 935 | func TestOverviewCascadeStashOnlyRight(t *testing.T) { | |
| 936 | 936 | m := overviewMonitor() | |
| 937 | 937 | m.Focus(1) | |
| @@ -344,7 +344,8 @@ func TestOverviewCascadeKeys(t *testing.T) { | |||||
| 344 | // With a stash: left from the first column still goes to the | 344 | // With a stash: left from the first column still goes to the | ||
| 345 | // previous workspace. Down past the last band enters the | 345 | // previous workspace. Down past the last band enters the | ||
| 346 | // stash, up comes back to the band, and left past the first | 346 | // stash, up comes back to the band, and left past the first | ||
| 347 | - | // card goes to the previous workspace. | 347 | + | // card goes to the previous workspace: two cards do not |
| 348 | + | // wrap round. | |||
| 348 | withStash(m, 2) | 349 | withStash(m, 2) | ||
| 349 | w.FocusID(7) | 350 | w.FocusID(7) | ||
| 350 | m.ToggleOverview() | 351 | m.ToggleOverview() | ||
| @@ -473,10 +474,9 @@ func TestOverviewCascadeStashPile(t *testing.T) { | |||||
| 473 | c.swipeUpdate(ports.SwipeUpdate{DX: 40}) | 474 | c.swipeUpdate(ports.SwipeUpdate{DX: 40}) | ||
| 474 | } | 475 | } | ||
| 475 | c.swipeEnd(ports.SwipeEnd{}) | 476 | c.swipeEnd(ports.SwipeEnd{}) | ||
| 476 | - | // A horizontal three-finger swipe is a workspace step, even over a | 477 | + | // A horizontal three-finger swipe walks the carousel, wrapping round. |
| 477 | - | // stash card. | 478 | + | if want := (at + 1) % len(w.Stash); m.Current() != w || m.cardAt(w) != want { |
| 478 | - | if m.Current() != m.Workspaces[1] { | 479 | + | t.Fatalf("horizontal swipe over a stash card: workspace %d, card %d, want %d", indexOf(m.Workspaces, m.Current()), m.cardAt(w), want) |
| 479 | - | t.Fatalf("horizontal swipe over a stash card: workspace %d", indexOf(m.Workspaces, m.Current())) | |||
| 480 | } | 480 | } | ||
| 481 | if got := stashIDs(w); !slices.Equal(got, before) { | 481 | if got := stashIDs(w); !slices.Equal(got, before) { | ||
| 482 | t.Fatal("stash reordered") | 482 | t.Fatal("stash reordered") | ||
| @@ -930,8 +930,8 @@ func TestOverviewFingerLatchSurvivesAxisChange(t *testing.T) { | |||||
| 930 | } | 930 | } | ||
| 931 | } | 931 | } | ||
| 932 | 932 | ||||
| 933 | - | // A stash-only cascade workspace has no columns to select: right from its | 933 | + | // A stash-only cascade workspace has nothing above its pile: right from its |
| 934 | - | // pile goes straight to the next workspace. | 934 | + | // pile goes straight to the next workspace, left to the previous one. |
| 935 | func TestOverviewCascadeStashOnlyRight(t *testing.T) { | 935 | func TestOverviewCascadeStashOnlyRight(t *testing.T) { | ||
| 936 | m := overviewMonitor() | 936 | m := overviewMonitor() | ||
| 937 | m.Focus(1) | 937 | m.Focus(1) | ||
M internal/core/overview_stash_input_test.go +32 -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,31 @@ 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, in its direction, wrapping round or not: the move | |
| 78 | + | // keys walk the cards. | |
| 79 | + | func TestOverviewWorkspaceBindLeavesCarousel(t *testing.T) { | |
| 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 | + | } | |
| 101 | + | } | |
| 102 | + | } | |
| @@ -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,31 @@ 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, in its direction, wrapping round or not: the move | |||
| 78 | + | // keys walk the cards. | |||
| 79 | + | func TestOverviewWorkspaceBindLeavesCarousel(t *testing.T) { | |||
| 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 | + | } | |||
| 101 | + | } | |||
| 102 | + | } | |||
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 +241 -65
| @@ -14,9 +14,10 @@ func shownCards(t *testing.T, w *Workspace, ps []Placement) []Placement { | |||
| 14 | 14 | return out | |
| 15 | 15 | } | |
| 16 | 16 | ||
| 17 | - | // Selected, the cascade pile unfolds into a row under the lane: every card | |
| 18 | - | // side by side, in stash order, none over another, the selection the only | |
| 19 | - | // framed one. Left and right walk it. | |
| 17 | + | // Selected, the cascade pile unfolds into a carousel under the lane: every | |
| 18 | + | // card side by side, none over another, the selection the only framed one, | |
| 19 | + | // in the middle. Left and right walk it, wrapping round, without leaving | |
| 20 | + | // the workspace. | |
| 20 | 21 | func TestOverviewCascadeStashUnfolds(t *testing.T) { | |
| 21 | 22 | for _, d := range cascadeDrivers { | |
| 22 | 23 | t.Run(d.name, func(t *testing.T) { | |
| @@ -36,42 +37,88 @@ func TestOverviewCascadeStashUnfolds(t *testing.T) { | |||
| 36 | 37 | t.Fatalf("%d cards shown, want 3", len(cards)) | |
| 37 | 38 | } | |
| 38 | 39 | lane := previewOf(t, ps, 10).Rect | |
| 39 | - | for i, c := range cards { | |
| 40 | + | // Left to right: the card before the selection, the | |
| 41 | + | // selection, the one after it (wrapping round). | |
| 42 | + | row := []Placement{cards[(at+2)%3], cards[at], cards[(at+1)%3]} | |
| 43 | + | for i, c := range row { | |
| 40 | 44 | if c.Rect.Y <= lane.Y+lane.H { | |
| 41 | 45 | t.Fatalf("card %d %v not under the lane %v", c.ID, c.Rect, lane) | |
| 42 | 46 | } | |
| 43 | - | if i > 0 && (c.Rect.Y != cards[0].Rect.Y || c.Rect.X < cards[i-1].Rect.X+cards[i-1].Rect.W) { | |
| 44 | - | t.Fatalf("cards not side by side: %v then %v", cards[i-1].Rect, c.Rect) | |
| 47 | + | if i > 0 && (c.Rect.Y != row[0].Rect.Y || c.Rect.X < row[i-1].Rect.X+row[i-1].Rect.W) { | |
| 48 | + | t.Fatalf("cards not side by side: %v then %v", row[i-1].Rect, c.Rect) | |
| 45 | 49 | } | |
| 46 | - | if c.Focused != (i == at) { | |
| 47 | - | t.Fatalf("card %d focused %v, selection %d", i, c.Focused, at) | |
| 50 | + | if c.Focused != (i == 1) { | |
| 51 | + | t.Fatalf("card %d focused %v, selection %d", c.ID, c.Focused, at) | |
| 48 | 52 | } | |
| 49 | 53 | } | |
| 50 | - | for i := range 2 { | |
| 54 | + | for i := range 4 { | |
| 51 | 55 | d.move(m, -1, 0) | |
| 52 | - | if want := max(at-i-1, 0); m.Current() != w || m.cardAt(w) != want { | |
| 56 | + | if want := (at - i - 1 + 6) % 3; m.Current() != w || m.cardAt(w) != want { | |
| 53 | 57 | t.Fatalf("left %d: card %d, want %d", i, m.cardAt(w), want) | |
| 54 | 58 | } | |
| 55 | - | if at-i-1 < 0 { | |
| 56 | - | break | |
| 57 | - | } | |
| 58 | 59 | } | |
| 59 | - | for m.cardAt(w) < len(w.Stash)-1 { | |
| 60 | + | for i := range 4 { | |
| 60 | 61 | before := m.cardAt(w) | |
| 61 | 62 | d.move(m, 1, 0) | |
| 62 | - | if m.cardAt(w) != before+1 { | |
| 63 | - | t.Fatalf("right: card %d, was %d", m.cardAt(w), before) | |
| 63 | + | if m.Current() != w || m.cardAt(w) != (before+1)%3 { | |
| 64 | + | t.Fatalf("right %d: card %d, was %d", i, m.cardAt(w), before) | |
| 64 | 65 | } | |
| 65 | 66 | } | |
| 66 | - | // Right past the last card goes to the next workspace. | |
| 67 | - | d.move(m, 1, 0) | |
| 68 | - | if m.Current() != m.Workspaces[2] { | |
| 69 | - | t.Fatal("right past the last card did not change workspace") | |
| 70 | - | } | |
| 71 | 67 | }) | |
| 72 | 68 | } | |
| 73 | 69 | } | |
| 74 | 70 | ||
| 71 | + | // Fewer than three cards keep the stash order without wrapping round: each | |
| 72 | + | // side has the card its move selects, and past the ends the selection | |
| 73 | + | // changes workspace, as from a lane. | |
| 74 | + | func TestOverviewCascadeSmallCarousel(t *testing.T) { | |
| 75 | + | for n := 1; n <= 2; n++ { | |
| 76 | + | m := cascadeOverview() | |
| 77 | + | withStash(m, n) | |
| 78 | + | w := m.Current() | |
| 79 | + | w.FocusID(10) | |
| 80 | + | m.ToggleOverview() | |
| 81 | + | m.selectCard(w, 0) | |
| 82 | + | if n == 2 { | |
| 83 | + | ps := m.Layout() | |
| 84 | + | if a, b := previewOf(t, ps, w.Stash[0].ID).Rect.X, previewOf(t, ps, w.Stash[1].ID).Rect.X; b <= a { | |
| 85 | + | t.Fatalf("2 cards: the next card at %d, not right of the selection at %d", b, a) | |
| 86 | + | } | |
| 87 | + | m.OverviewMove(1, 0) | |
| 88 | + | if m.Current() != w || m.cardAt(w) != 1 { | |
| 89 | + | t.Fatalf("2 cards: right selected card %d", m.cardAt(w)) | |
| 90 | + | } | |
| 91 | + | } | |
| 92 | + | m.OverviewMove(1, 0) | |
| 93 | + | if m.Current() != m.Workspaces[2] { | |
| 94 | + | t.Fatalf("%d cards: right past the last card stayed on workspace %d", n, indexOf(m.Workspaces, m.Current())) | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // The carousel keeps the stash order, wrapping round: the stride between | |
| 100 | + | // neighbors is even, wider at the seam between the newest card and the | |
| 101 | + | // oldest. | |
| 102 | + | func TestOverviewCascadeCarouselSeam(t *testing.T) { | |
| 103 | + | m := cascadeOverview() | |
| 104 | + | withStash(m, 5) | |
| 105 | + | w := m.Current() | |
| 106 | + | m.ToggleOverview() | |
| 107 | + | m.selectCard(w, 4) | |
| 108 | + | ps := m.Layout() | |
| 109 | + | x := func(i int) int { return previewOf(t, ps, w.Stash[i].ID).Rect.X } | |
| 110 | + | stride := x(4) - x(3) | |
| 111 | + | if stride <= 0 || x(3)-x(2) != stride { | |
| 112 | + | t.Fatalf("left of the newest card: %d %d %d", x(2), x(3), x(4)) | |
| 113 | + | } | |
| 114 | + | if seam := x(0) - x(4); seam <= stride { | |
| 115 | + | t.Fatalf("seam %d not wider than the stride %d", seam, stride) | |
| 116 | + | } | |
| 117 | + | if x(1)-x(0) != stride { | |
| 118 | + | t.Fatalf("right of the seam: %d %d", x(0), x(1)) | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | ||
| 75 | 122 | // Down past the last lane enters the stash at its own selection; up leaves | |
| 76 | 123 | // it, and the pile folds again. | |
| 77 | 124 | func TestOverviewCascadeStashFolds(t *testing.T) { | |
| @@ -97,11 +144,10 @@ func TestOverviewCascadeStashFolds(t *testing.T) { | |||
| 97 | 144 | } | |
| 98 | 145 | } | |
| 99 | 146 | ||
| 100 | - | // A stash longer than the area scrolls like a wide row, on either axis: | |
| 101 | - | // every card is reached in order, the selection stays inside the area and | |
| 102 | - | // is centred unless the strip stops at an edge of it, and cards past the | |
| 103 | - | // area are hidden. | |
| 104 | - | func TestOverviewStashUnfoldScrolls(t *testing.T) { | |
| 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) { | |
| 105 | 151 | for _, axis := range []layoutAxis{horizontalAxis, verticalAxis} { | |
| 106 | 152 | t.Run(map[layoutAxis]string{horizontalAxis: "cascade", verticalAxis: "scroll"}[axis], func(t *testing.T) { | |
| 107 | 153 | m := cascadeOverview() | |
| @@ -114,47 +160,41 @@ func TestOverviewStashUnfoldScrolls(t *testing.T) { | |||
| 114 | 160 | if m.overviewAxis() != axis { | |
| 115 | 161 | t.Fatal("setup: wrong overview axis") | |
| 116 | 162 | } | |
| 117 | - | m.selectCard(w, 0) | |
| 163 | + | m.selectCard(w, 11) | |
| 118 | 164 | u := w.overviewArea() | |
| 119 | - | pos := func(r Rect) (int, int, int, int) { | |
| 165 | + | pos := func(r Rect) (int, int) { | |
| 120 | 166 | if axis == verticalAxis { | |
| 121 | - | return r.Y, r.H, u.Y, u.Y + u.H | |
| 167 | + | return r.Y, r.Y + r.H/2 - (u.Y + u.H/2) | |
| 122 | 168 | } | |
| 123 | - | return r.X, r.W, u.X, u.X + u.W | |
| 169 | + | return r.X, r.X + r.W/2 - (u.X + u.W/2) | |
| 124 | 170 | } | |
| 125 | - | centred, hidden := false, false | |
| 126 | - | for i := range len(w.Stash) { | |
| 127 | - | if m.cardAt(w) != i { | |
| 128 | - | t.Fatalf("step %d: card %d", i, m.cardAt(w)) | |
| 129 | - | } | |
| 171 | + | for step := range len(w.Stash) + 1 { | |
| 172 | + | i := m.cardAt(w) | |
| 130 | 173 | ps := m.Layout() | |
| 131 | 174 | sel := previewOf(t, ps, w.Stash[i].ID) | |
| 132 | - | at, size, lo, hi := pos(sel.Rect) | |
| 133 | - | if sel.Hidden || !sel.Focused || at < lo || at+size > hi { | |
| 134 | - | t.Fatalf("card %d off the area: %+v in %v", i, sel, u) | |
| 135 | - | } | |
| 136 | - | first, _, _, _ := pos(previewOf(t, ps, w.Stash[0].ID).Rect) | |
| 137 | - | last, _, _, _ := pos(previewOf(t, ps, w.Stash[len(w.Stash)-1].ID).Rect) | |
| 138 | - | if first > lo || last+size < hi { | |
| 139 | - | t.Fatalf("card %d: strip %d..%d does not fill the area %d..%d", i, first, last+size, lo, hi) | |
| 140 | - | } | |
| 141 | - | if mid := at + size/2; abs(mid-(lo+hi)/2) <= 1 { | |
| 142 | - | centred = true | |
| 143 | - | } else if first != lo && last+size != hi { | |
| 144 | - | t.Fatalf("card %d at %d, neither centred on %d nor clamped", i, mid, (lo+hi)/2) | |
| 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) | |
| 145 | 178 | } | |
| 179 | + | before, after, hidden := false, false, false | |
| 146 | 180 | for _, e := range w.Stash { | |
| 147 | - | hidden = hidden || previewOf(t, ps, e.ID).Hidden | |
| 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) | |
| 148 | 191 | } | |
| 149 | 192 | if axis == verticalAxis { | |
| 150 | 193 | m.OverviewMove(0, 1) | |
| 151 | - | } else if i < len(w.Stash)-1 { | |
| 194 | + | } else { | |
| 152 | 195 | m.OverviewMove(1, 0) | |
| 153 | 196 | } | |
| 154 | 197 | } | |
| 155 | - | if !hidden || !centred { | |
| 156 | - | t.Fatalf("12 cards: hidden %v, centred %v; the test does not scroll", hidden, centred) | |
| 157 | - | } | |
| 158 | 198 | }) | |
| 159 | 199 | } | |
| 160 | 200 | } | |
| @@ -183,8 +223,10 @@ func TestOverviewStashLayoutAllocations(t *testing.T) { | |||
| 183 | 223 | } | |
| 184 | 224 | } | |
| 185 | 225 | ||
| 186 | - | // A scroll overview unfolds its pile into a column on the left of the row; | |
| 187 | - | // 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. | |
| 188 | 230 | func TestOverviewScrollStashUnfolds(t *testing.T) { | |
| 189 | 231 | m := overviewMonitor() | |
| 190 | 232 | w := m.Current() | |
| @@ -199,29 +241,163 @@ func TestOverviewScrollStashUnfolds(t *testing.T) { | |||
| 199 | 241 | t.Fatal("setup: not a vertical overview") | |
| 200 | 242 | } | |
| 201 | 243 | m.OverviewMove(-1, 0) | |
| 202 | - | if m.cardAt(w) < 0 { | |
| 244 | + | at := m.cardAt(w) | |
| 245 | + | if at < 0 { | |
| 203 | 246 | t.Fatal("left from the first column did not select the stash") | |
| 204 | 247 | } | |
| 205 | 248 | cards := shownCards(t, w, m.Layout()) | |
| 206 | 249 | if len(cards) != 3 { | |
| 207 | 250 | t.Fatalf("%d cards shown, want 3", len(cards)) | |
| 208 | 251 | } | |
| 209 | - | for i := 1; i < len(cards); i++ { | |
| 210 | - | if cards[i].Rect.X != cards[0].Rect.X || cards[i].Rect.Y < cards[i-1].Rect.Y+cards[i-1].Rect.H { | |
| 211 | - | 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) | |
| 212 | 256 | } | |
| 213 | 257 | } | |
| 214 | - | m.OverviewMove(0, -1) | |
| 215 | - | m.OverviewMove(0, -1) | |
| 216 | - | if m.cardAt(w) != 0 { | |
| 217 | - | 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") | |
| 218 | 260 | } | |
| 219 | 261 | m.OverviewMove(0, 1) | |
| 220 | - | if m.cardAt(w) != 1 { | |
| 221 | - | 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) | |
| 222 | 269 | } | |
| 223 | 270 | m.OverviewMove(1, 0) | |
| 224 | 271 | if m.cardAt(w) >= 0 || m.overviewTarget() == 0 { | |
| 225 | 272 | t.Fatalf("right: card %d, target %d", m.cardAt(w), m.overviewTarget()) | |
| 226 | 273 | } | |
| 227 | 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 | + | } | |
| 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 | + | } | |
| 362 | + | ||
| 363 | + | // Stepping to a workspace whose stash has the focus selects its stack when | |
| 364 | + | // the carousel wraps round, so the next step goes on to the next workspace; | |
| 365 | + | // a carousel that does not wrap is still selected. | |
| 366 | + | func TestOverviewStepSkipsWrappingCarousel(t *testing.T) { | |
| 367 | + | for _, overflow := range []Overflow{OverflowCascade, OverflowScroll} { | |
| 368 | + | for _, n := range []int{2, 3} { | |
| 369 | + | m := overviewMonitor() | |
| 370 | + | m.Focus(2) | |
| 371 | + | m.AddWindow(30) | |
| 372 | + | m.Focus(1) | |
| 373 | + | w := m.Current() | |
| 374 | + | for id := WindowID(40); id < WindowID(40+n); id++ { | |
| 375 | + | w.AddWindow(id) | |
| 376 | + | w.FocusID(id) | |
| 377 | + | w.ToggleWindowStash() | |
| 378 | + | } | |
| 379 | + | for _, ws := range m.Workspaces[:3] { | |
| 380 | + | ws.Overflow = overflow | |
| 381 | + | } | |
| 382 | + | m.Focus(0) | |
| 383 | + | if !w.stashFocused() { | |
| 384 | + | t.Fatal("setup: the stash does not have the focus") | |
| 385 | + | } | |
| 386 | + | m.ToggleOverview() | |
| 387 | + | m.overviewFocus(ActionFocusWorkspaceNext) | |
| 388 | + | if m.Current() != w { | |
| 389 | + | t.Fatalf("%s, %d cards: on workspace %d", overflow, n, indexOf(m.Workspaces, m.Current())) | |
| 390 | + | } | |
| 391 | + | if got, want := m.cardAt(w) >= 0, n < 3; got != want { | |
| 392 | + | t.Fatalf("%s, %d cards: card selected %v, want %v", overflow, n, got, want) | |
| 393 | + | } | |
| 394 | + | if n < 3 { | |
| 395 | + | continue | |
| 396 | + | } | |
| 397 | + | m.overviewFocus(ActionFocusWorkspaceNext) | |
| 398 | + | if m.Current() != m.Workspaces[2] { | |
| 399 | + | t.Fatalf("%s: the next step stayed on workspace %d", overflow, indexOf(m.Workspaces, m.Current())) | |
| 400 | + | } | |
| 401 | + | } | |
| 402 | + | } | |
| 403 | + | } | |
| @@ -14,9 +14,10 @@ func shownCards(t *testing.T, w *Workspace, ps []Placement) []Placement { | |||||
| 14 | return out | 14 | return out | ||
| 15 | } | 15 | } | ||
| 16 | 16 | ||||
| 17 | - | // Selected, the cascade pile unfolds into a row under the lane: every card | 17 | + | // Selected, the cascade pile unfolds into a carousel under the lane: every |
| 18 | - | // side by side, in stash order, none over another, the selection the only | 18 | + | // card side by side, none over another, the selection the only framed one, |
| 19 | - | // framed one. Left and right walk it. | 19 | + | // in the middle. Left and right walk it, wrapping round, without leaving |
| 20 | + | // the workspace. | |||
| 20 | func TestOverviewCascadeStashUnfolds(t *testing.T) { | 21 | func TestOverviewCascadeStashUnfolds(t *testing.T) { | ||
| 21 | for _, d := range cascadeDrivers { | 22 | for _, d := range cascadeDrivers { | ||
| 22 | t.Run(d.name, func(t *testing.T) { | 23 | t.Run(d.name, func(t *testing.T) { | ||
| @@ -36,42 +37,88 @@ func TestOverviewCascadeStashUnfolds(t *testing.T) { | |||||
| 36 | t.Fatalf("%d cards shown, want 3", len(cards)) | 37 | t.Fatalf("%d cards shown, want 3", len(cards)) | ||
| 37 | } | 38 | } | ||
| 38 | lane := previewOf(t, ps, 10).Rect | 39 | lane := previewOf(t, ps, 10).Rect | ||
| 39 | - | for i, c := range cards { | 40 | + | // Left to right: the card before the selection, the |
| 41 | + | // selection, the one after it (wrapping round). | |||
| 42 | + | row := []Placement{cards[(at+2)%3], cards[at], cards[(at+1)%3]} | |||
| 43 | + | for i, c := range row { | |||
| 40 | if c.Rect.Y <= lane.Y+lane.H { | 44 | if c.Rect.Y <= lane.Y+lane.H { | ||
| 41 | t.Fatalf("card %d %v not under the lane %v", c.ID, c.Rect, lane) | 45 | t.Fatalf("card %d %v not under the lane %v", c.ID, c.Rect, lane) | ||
| 42 | } | 46 | } | ||
| 43 | - | if i > 0 && (c.Rect.Y != cards[0].Rect.Y || c.Rect.X < cards[i-1].Rect.X+cards[i-1].Rect.W) { | 47 | + | if i > 0 && (c.Rect.Y != row[0].Rect.Y || c.Rect.X < row[i-1].Rect.X+row[i-1].Rect.W) { |
| 44 | - | t.Fatalf("cards not side by side: %v then %v", cards[i-1].Rect, c.Rect) | 48 | + | t.Fatalf("cards not side by side: %v then %v", row[i-1].Rect, c.Rect) |
| 45 | } | 49 | } | ||
| 46 | - | if c.Focused != (i == at) { | 50 | + | if c.Focused != (i == 1) { |
| 47 | - | t.Fatalf("card %d focused %v, selection %d", i, c.Focused, at) | 51 | + | t.Fatalf("card %d focused %v, selection %d", c.ID, c.Focused, at) |
| 48 | } | 52 | } | ||
| 49 | } | 53 | } | ||
| 50 | - | for i := range 2 { | 54 | + | for i := range 4 { |
| 51 | d.move(m, -1, 0) | 55 | d.move(m, -1, 0) | ||
| 52 | - | if want := max(at-i-1, 0); m.Current() != w || m.cardAt(w) != want { | 56 | + | if want := (at - i - 1 + 6) % 3; m.Current() != w || m.cardAt(w) != want { |
| 53 | t.Fatalf("left %d: card %d, want %d", i, m.cardAt(w), want) | 57 | t.Fatalf("left %d: card %d, want %d", i, m.cardAt(w), want) | ||
| 54 | } | 58 | } | ||
| 55 | - | if at-i-1 < 0 { | |||
| 56 | - | break | |||
| 57 | - | } | |||
| 58 | } | 59 | } | ||
| 59 | - | for m.cardAt(w) < len(w.Stash)-1 { | 60 | + | for i := range 4 { |
| 60 | before := m.cardAt(w) | 61 | before := m.cardAt(w) | ||
| 61 | d.move(m, 1, 0) | 62 | d.move(m, 1, 0) | ||
| 62 | - | if m.cardAt(w) != before+1 { | 63 | + | if m.Current() != w || m.cardAt(w) != (before+1)%3 { |
| 63 | - | t.Fatalf("right: card %d, was %d", m.cardAt(w), before) | 64 | + | t.Fatalf("right %d: card %d, was %d", i, m.cardAt(w), before) |
| 64 | } | 65 | } | ||
| 65 | } | 66 | } | ||
| 66 | - | // Right past the last card goes to the next workspace. | |||
| 67 | - | d.move(m, 1, 0) | |||
| 68 | - | if m.Current() != m.Workspaces[2] { | |||
| 69 | - | t.Fatal("right past the last card did not change workspace") | |||
| 70 | - | } | |||
| 71 | }) | 67 | }) | ||
| 72 | } | 68 | } | ||
| 73 | } | 69 | } | ||
| 74 | 70 | ||||
| 71 | + | // Fewer than three cards keep the stash order without wrapping round: each | |||
| 72 | + | // side has the card its move selects, and past the ends the selection | |||
| 73 | + | // changes workspace, as from a lane. | |||
| 74 | + | func TestOverviewCascadeSmallCarousel(t *testing.T) { | |||
| 75 | + | for n := 1; n <= 2; n++ { | |||
| 76 | + | m := cascadeOverview() | |||
| 77 | + | withStash(m, n) | |||
| 78 | + | w := m.Current() | |||
| 79 | + | w.FocusID(10) | |||
| 80 | + | m.ToggleOverview() | |||
| 81 | + | m.selectCard(w, 0) | |||
| 82 | + | if n == 2 { | |||
| 83 | + | ps := m.Layout() | |||
| 84 | + | if a, b := previewOf(t, ps, w.Stash[0].ID).Rect.X, previewOf(t, ps, w.Stash[1].ID).Rect.X; b <= a { | |||
| 85 | + | t.Fatalf("2 cards: the next card at %d, not right of the selection at %d", b, a) | |||
| 86 | + | } | |||
| 87 | + | m.OverviewMove(1, 0) | |||
| 88 | + | if m.Current() != w || m.cardAt(w) != 1 { | |||
| 89 | + | t.Fatalf("2 cards: right selected card %d", m.cardAt(w)) | |||
| 90 | + | } | |||
| 91 | + | } | |||
| 92 | + | m.OverviewMove(1, 0) | |||
| 93 | + | if m.Current() != m.Workspaces[2] { | |||
| 94 | + | t.Fatalf("%d cards: right past the last card stayed on workspace %d", n, indexOf(m.Workspaces, m.Current())) | |||
| 95 | + | } | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // The carousel keeps the stash order, wrapping round: the stride between | |||
| 100 | + | // neighbors is even, wider at the seam between the newest card and the | |||
| 101 | + | // oldest. | |||
| 102 | + | func TestOverviewCascadeCarouselSeam(t *testing.T) { | |||
| 103 | + | m := cascadeOverview() | |||
| 104 | + | withStash(m, 5) | |||
| 105 | + | w := m.Current() | |||
| 106 | + | m.ToggleOverview() | |||
| 107 | + | m.selectCard(w, 4) | |||
| 108 | + | ps := m.Layout() | |||
| 109 | + | x := func(i int) int { return previewOf(t, ps, w.Stash[i].ID).Rect.X } | |||
| 110 | + | stride := x(4) - x(3) | |||
| 111 | + | if stride <= 0 || x(3)-x(2) != stride { | |||
| 112 | + | t.Fatalf("left of the newest card: %d %d %d", x(2), x(3), x(4)) | |||
| 113 | + | } | |||
| 114 | + | if seam := x(0) - x(4); seam <= stride { | |||
| 115 | + | t.Fatalf("seam %d not wider than the stride %d", seam, stride) | |||
| 116 | + | } | |||
| 117 | + | if x(1)-x(0) != stride { | |||
| 118 | + | t.Fatalf("right of the seam: %d %d", x(0), x(1)) | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 121 | + | ||||
| 75 | // Down past the last lane enters the stash at its own selection; up leaves | 122 | // Down past the last lane enters the stash at its own selection; up leaves | ||
| 76 | // it, and the pile folds again. | 123 | // it, and the pile folds again. | ||
| 77 | func TestOverviewCascadeStashFolds(t *testing.T) { | 124 | func TestOverviewCascadeStashFolds(t *testing.T) { | ||
| @@ -97,11 +144,10 @@ func TestOverviewCascadeStashFolds(t *testing.T) { | |||||
| 97 | } | 144 | } | ||
| 98 | } | 145 | } | ||
| 99 | 146 | ||||
| 100 | - | // A stash longer than the area scrolls like a wide row, on either axis: | 147 | + | // A carousel longer than the area keeps every selection centred in it, |
| 101 | - | // every card is reached in order, the selection stays inside the area and | 148 | + | // with cards on both sides, on either axis; the cards past the area are |
| 102 | - | // is centred unless the strip stops at an edge of it, and cards past the | 149 | + | // hidden. |
| 103 | - | // area are hidden. | 150 | + | func TestOverviewCarouselScrolls(t *testing.T) { |
| 104 | - | func TestOverviewStashUnfoldScrolls(t *testing.T) { | |||
| 105 | for _, axis := range []layoutAxis{horizontalAxis, verticalAxis} { | 151 | for _, axis := range []layoutAxis{horizontalAxis, verticalAxis} { | ||
| 106 | t.Run(map[layoutAxis]string{horizontalAxis: "cascade", verticalAxis: "scroll"}[axis], func(t *testing.T) { | 152 | t.Run(map[layoutAxis]string{horizontalAxis: "cascade", verticalAxis: "scroll"}[axis], func(t *testing.T) { | ||
| 107 | m := cascadeOverview() | 153 | m := cascadeOverview() | ||
| @@ -114,47 +160,41 @@ func TestOverviewStashUnfoldScrolls(t *testing.T) { | |||||
| 114 | if m.overviewAxis() != axis { | 160 | if m.overviewAxis() != axis { | ||
| 115 | t.Fatal("setup: wrong overview axis") | 161 | t.Fatal("setup: wrong overview axis") | ||
| 116 | } | 162 | } | ||
| 117 | - | m.selectCard(w, 0) | 163 | + | m.selectCard(w, 11) |
| 118 | u := w.overviewArea() | 164 | u := w.overviewArea() | ||
| 119 | - | pos := func(r Rect) (int, int, int, int) { | 165 | + | pos := func(r Rect) (int, int) { |
| 120 | if axis == verticalAxis { | 166 | if axis == verticalAxis { | ||
| 121 | - | return r.Y, r.H, u.Y, u.Y + u.H | 167 | + | return r.Y, r.Y + r.H/2 - (u.Y + u.H/2) |
| 122 | } | 168 | } | ||
| 123 | - | return r.X, r.W, u.X, u.X + u.W | 169 | + | return r.X, r.X + r.W/2 - (u.X + u.W/2) |
| 124 | } | 170 | } | ||
| 125 | - | centred, hidden := false, false | 171 | + | for step := range len(w.Stash) + 1 { |
| 126 | - | for i := range len(w.Stash) { | 172 | + | i := m.cardAt(w) |
| 127 | - | if m.cardAt(w) != i { | |||
| 128 | - | t.Fatalf("step %d: card %d", i, m.cardAt(w)) | |||
| 129 | - | } | |||
| 130 | ps := m.Layout() | 173 | ps := m.Layout() | ||
| 131 | sel := previewOf(t, ps, w.Stash[i].ID) | 174 | sel := previewOf(t, ps, w.Stash[i].ID) | ||
| 132 | - | at, size, lo, hi := pos(sel.Rect) | 175 | + | at, off := pos(sel.Rect) |
| 133 | - | if sel.Hidden || !sel.Focused || at < lo || at+size > hi { | 176 | + | if sel.Hidden || !sel.Focused || abs(off) > 1 { |
| 134 | - | t.Fatalf("card %d off the area: %+v in %v", i, sel, u) | 177 | + | t.Fatalf("step %d: card %d not centred: %+v in %v", step, i, sel, u) |
| 135 | - | } | |||
| 136 | - | first, _, _, _ := pos(previewOf(t, ps, w.Stash[0].ID).Rect) | |||
| 137 | - | last, _, _, _ := pos(previewOf(t, ps, w.Stash[len(w.Stash)-1].ID).Rect) | |||
| 138 | - | if first > lo || last+size < hi { | |||
| 139 | - | t.Fatalf("card %d: strip %d..%d does not fill the area %d..%d", i, first, last+size, lo, hi) | |||
| 140 | - | } | |||
| 141 | - | if mid := at + size/2; abs(mid-(lo+hi)/2) <= 1 { | |||
| 142 | - | centred = true | |||
| 143 | - | } else if first != lo && last+size != hi { | |||
| 144 | - | t.Fatalf("card %d at %d, neither centred on %d nor clamped", i, mid, (lo+hi)/2) | |||
| 145 | } | 178 | } | ||
| 179 | + | before, after, hidden := false, false, false | |||
| 146 | for _, e := range w.Stash { | 180 | for _, e := range w.Stash { | ||
| 147 | - | hidden = hidden || previewOf(t, ps, e.ID).Hidden | 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) | |||
| 148 | } | 191 | } | ||
| 149 | if axis == verticalAxis { | 192 | if axis == verticalAxis { | ||
| 150 | m.OverviewMove(0, 1) | 193 | m.OverviewMove(0, 1) | ||
| 151 | - | } else if i < len(w.Stash)-1 { | 194 | + | } else { |
| 152 | m.OverviewMove(1, 0) | 195 | m.OverviewMove(1, 0) | ||
| 153 | } | 196 | } | ||
| 154 | } | 197 | } | ||
| 155 | - | if !hidden || !centred { | |||
| 156 | - | t.Fatalf("12 cards: hidden %v, centred %v; the test does not scroll", hidden, centred) | |||
| 157 | - | } | |||
| 158 | }) | 198 | }) | ||
| 159 | } | 199 | } | ||
| 160 | } | 200 | } | ||
| @@ -183,8 +223,10 @@ func TestOverviewStashLayoutAllocations(t *testing.T) { | |||||
| 183 | } | 223 | } | ||
| 184 | } | 224 | } | ||
| 185 | 225 | ||||
| 186 | - | // 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 |
| 187 | - | // 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. | |||
| 188 | func TestOverviewScrollStashUnfolds(t *testing.T) { | 230 | func TestOverviewScrollStashUnfolds(t *testing.T) { | ||
| 189 | m := overviewMonitor() | 231 | m := overviewMonitor() | ||
| 190 | w := m.Current() | 232 | w := m.Current() | ||
| @@ -199,29 +241,163 @@ func TestOverviewScrollStashUnfolds(t *testing.T) { | |||||
| 199 | t.Fatal("setup: not a vertical overview") | 241 | t.Fatal("setup: not a vertical overview") | ||
| 200 | } | 242 | } | ||
| 201 | m.OverviewMove(-1, 0) | 243 | m.OverviewMove(-1, 0) | ||
| 202 | - | if m.cardAt(w) < 0 { | 244 | + | at := m.cardAt(w) |
| 245 | + | if at < 0 { | |||
| 203 | 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") | ||
| 204 | } | 247 | } | ||
| 205 | cards := shownCards(t, w, m.Layout()) | 248 | cards := shownCards(t, w, m.Layout()) | ||
| 206 | if len(cards) != 3 { | 249 | if len(cards) != 3 { | ||
| 207 | t.Fatalf("%d cards shown, want 3", len(cards)) | 250 | t.Fatalf("%d cards shown, want 3", len(cards)) | ||
| 208 | } | 251 | } | ||
| 209 | - | for i := 1; i < len(cards); i++ { | 252 | + | col := []Placement{cards[(at+2)%3], cards[at], cards[(at+1)%3]} |
| 210 | - | 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++ { |
| 211 | - | 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) | |||
| 212 | } | 256 | } | ||
| 213 | } | 257 | } | ||
| 214 | - | m.OverviewMove(0, -1) | 258 | + | if !col[1].Focused { |
| 215 | - | m.OverviewMove(0, -1) | 259 | + | t.Fatal("the selection is not in the middle") |
| 216 | - | if m.cardAt(w) != 0 { | |||
| 217 | - | t.Fatalf("up: card %d, want 0", m.cardAt(w)) | |||
| 218 | } | 260 | } | ||
| 219 | m.OverviewMove(0, 1) | 261 | m.OverviewMove(0, 1) | ||
| 220 | - | if m.cardAt(w) != 1 { | 262 | + | if want := (at + 1) % 3; m.cardAt(w) != want { |
| 221 | - | 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) | |||
| 222 | } | 269 | } | ||
| 223 | m.OverviewMove(1, 0) | 270 | m.OverviewMove(1, 0) | ||
| 224 | if m.cardAt(w) >= 0 || m.overviewTarget() == 0 { | 271 | if m.cardAt(w) >= 0 || m.overviewTarget() == 0 { | ||
| 225 | 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()) | ||
| 226 | } | 273 | } | ||
| 227 | } | 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 | + | } | |||
| 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 | + | } | |||
| 362 | + | ||||
| 363 | + | // Stepping to a workspace whose stash has the focus selects its stack when | |||
| 364 | + | // the carousel wraps round, so the next step goes on to the next workspace; | |||
| 365 | + | // a carousel that does not wrap is still selected. | |||
| 366 | + | func TestOverviewStepSkipsWrappingCarousel(t *testing.T) { | |||
| 367 | + | for _, overflow := range []Overflow{OverflowCascade, OverflowScroll} { | |||
| 368 | + | for _, n := range []int{2, 3} { | |||
| 369 | + | m := overviewMonitor() | |||
| 370 | + | m.Focus(2) | |||
| 371 | + | m.AddWindow(30) | |||
| 372 | + | m.Focus(1) | |||
| 373 | + | w := m.Current() | |||
| 374 | + | for id := WindowID(40); id < WindowID(40+n); id++ { | |||
| 375 | + | w.AddWindow(id) | |||
| 376 | + | w.FocusID(id) | |||
| 377 | + | w.ToggleWindowStash() | |||
| 378 | + | } | |||
| 379 | + | for _, ws := range m.Workspaces[:3] { | |||
| 380 | + | ws.Overflow = overflow | |||
| 381 | + | } | |||
| 382 | + | m.Focus(0) | |||
| 383 | + | if !w.stashFocused() { | |||
| 384 | + | t.Fatal("setup: the stash does not have the focus") | |||
| 385 | + | } | |||
| 386 | + | m.ToggleOverview() | |||
| 387 | + | m.overviewFocus(ActionFocusWorkspaceNext) | |||
| 388 | + | if m.Current() != w { | |||
| 389 | + | t.Fatalf("%s, %d cards: on workspace %d", overflow, n, indexOf(m.Workspaces, m.Current())) | |||
| 390 | + | } | |||
| 391 | + | if got, want := m.cardAt(w) >= 0, n < 3; got != want { | |||
| 392 | + | t.Fatalf("%s, %d cards: card selected %v, want %v", overflow, n, got, want) | |||
| 393 | + | } | |||
| 394 | + | if n < 3 { | |||
| 395 | + | continue | |||
| 396 | + | } | |||
| 397 | + | m.overviewFocus(ActionFocusWorkspaceNext) | |||
| 398 | + | if m.Current() != m.Workspaces[2] { | |||
| 399 | + | t.Fatalf("%s: the next step stayed on workspace %d", overflow, indexOf(m.Workspaces, m.Current())) | |||
| 400 | + | } | |||
| 401 | + | } | |||
| 402 | + | } | |||
| 403 | + | } | |||
M internal/core/transition.go +37 -0
| @@ -45,6 +45,11 @@ type viewShot struct { | |||
| 45 | 45 | stashShown bool | |
| 46 | 46 | stashAt, stashLen int | |
| 47 | 47 | stashPos, stashV float64 | |
| 48 | + | // card is the selected card of the unfolded stash carousel in the | |
| 49 | + | // overview (Workspace.unfold), -1 without one; wraps is whether that | |
| 50 | + | // carousel wrapped round (Workspace.wrapsCards). | |
| 51 | + | card int | |
| 52 | + | wraps bool | |
| 48 | 53 | // list and pos are the numbered workspaces and the monitor's fractional | |
| 49 | 54 | // position in them (index of the current one plus switchView.off); ok is | |
| 50 | 55 | // false when there is none (a shown stash workspace). | |
| @@ -637,6 +642,7 @@ func (c *Core) snapshot(now time.Time) []viewShot { | |||
| 637 | 642 | _, s.switchV = m.switchView.motion.sampleAt(now) | |
| 638 | 643 | } | |
| 639 | 644 | s.stashShown, s.stashAt, s.stashLen, s.stashPos = w.stashShown(), w.stashAt, len(w.Stash), float64(w.stashAt)+w.stashView.off | |
| 645 | + | s.card, s.wraps = m.cardAt(w), w.wrapsCards() | |
| 640 | 646 | if w.stashView.motion.on { | |
| 641 | 647 | _, s.stashV = w.stashView.motion.sampleAt(now) | |
| 642 | 648 | } | |
| @@ -903,12 +909,23 @@ func (c *Core) transitionOverview(b *viewShot, now time.Time) { | |||
| 903 | 909 | if changed { | |
| 904 | 910 | sc.stopRects() | |
| 905 | 911 | } | |
| 912 | + | at := -1 | |
| 913 | + | if !changed { | |
| 914 | + | at = carouselStep(b) | |
| 915 | + | } | |
| 916 | + | w := sc.mon.Current() | |
| 917 | + | n := len(w.Stash) | |
| 906 | 918 | for _, p := range sc.mon.Layout() { | |
| 907 | 919 | if p.Hidden { | |
| 908 | 920 | continue | |
| 909 | 921 | } | |
| 910 | 922 | rm := sc.rects[p.ID] | |
| 911 | 923 | i := slices.IndexFunc(b.rects, func(r rectShot) bool { return r.id == p.ID }) | |
| 924 | + | if i >= 0 && at >= 0 && wrapped(w.stashIndex(p.ID), b.card, at, n, b.wraps) { | |
| 925 | + | // The card went round the carousel to its other end: it | |
| 926 | + | // fades in there instead of crossing the row. | |
| 927 | + | rm, i = rectMotion{}, -1 | |
| 928 | + | } | |
| 912 | 929 | if i >= 0 && changed { | |
| 913 | 930 | c.retargetLevel(&rm.fade, &rm.df, b.rects[i].fade, b.rects[i].fadeV, now) | |
| 914 | 931 | } | |
| @@ -934,6 +951,26 @@ func (c *Core) transitionOverview(b *viewShot, now time.Time) { | |||
| 934 | 951 | } | |
| 935 | 952 | } | |
| 936 | 953 | ||
| 954 | + | // carouselStep is the selected card of the stash carousel b's screen shows | |
| 955 | + | // now, when it showed the same carousel in b too (the same workspace, stash | |
| 956 | + | // size and wrapping); else -1. | |
| 957 | + | func carouselStep(b *viewShot) int { | |
| 958 | + | m := b.sc.mon | |
| 959 | + | w := m.Current() | |
| 960 | + | if b.card < 0 || w != b.ws || len(w.Stash) != b.stashLen || w.wrapsCards() != b.wraps { | |
| 961 | + | return -1 | |
| 962 | + | } | |
| 963 | + | return m.cardAt(w) | |
| 964 | + | } | |
| 965 | + | ||
| 966 | + | // wrapped reports whether stash card i of a carousel of n cards went round | |
| 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). | |
| 970 | + | func wrapped(i, from, to, n int, wraps bool) bool { | |
| 971 | + | return i >= 0 && carouselSlot(i, to, n, wraps)-carouselSlot(i, from, n, wraps) != carouselSlot(from, to, n, wraps) | |
| 972 | + | } | |
| 973 | + | ||
| 937 | 974 | // movedFrom is the shot of the other screen that showed the workspace b's | |
| 938 | 975 | // screen shows now (moved to it by an action), or nil. | |
| 939 | 976 | func movedFrom(b *viewShot, before []viewShot) *viewShot { | |
| @@ -45,6 +45,11 @@ 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 the unfolded stash carousel in the | |||
| 49 | + | // overview (Workspace.unfold), -1 without one; wraps is whether that | |||
| 50 | + | // carousel wrapped round (Workspace.wrapsCards). | |||
| 51 | + | card int | |||
| 52 | + | wraps bool | |||
| 48 | // 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 | ||
| 49 | // 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 | ||
| 50 | // false when there is none (a shown stash workspace). | 55 | // false when there is none (a shown stash workspace). | ||
| @@ -637,6 +642,7 @@ func (c *Core) snapshot(now time.Time) []viewShot { | |||||
| 637 | _, s.switchV = m.switchView.motion.sampleAt(now) | 642 | _, s.switchV = m.switchView.motion.sampleAt(now) | ||
| 638 | } | 643 | } | ||
| 639 | 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 | ||
| 645 | + | s.card, s.wraps = m.cardAt(w), w.wrapsCards() | |||
| 640 | if w.stashView.motion.on { | 646 | if w.stashView.motion.on { | ||
| 641 | _, s.stashV = w.stashView.motion.sampleAt(now) | 647 | _, s.stashV = w.stashView.motion.sampleAt(now) | ||
| 642 | } | 648 | } | ||
| @@ -903,12 +909,23 @@ func (c *Core) transitionOverview(b *viewShot, now time.Time) { | |||||
| 903 | if changed { | 909 | if changed { | ||
| 904 | sc.stopRects() | 910 | sc.stopRects() | ||
| 905 | } | 911 | } | ||
| 912 | + | at := -1 | |||
| 913 | + | if !changed { | |||
| 914 | + | at = carouselStep(b) | |||
| 915 | + | } | |||
| 916 | + | w := sc.mon.Current() | |||
| 917 | + | n := len(w.Stash) | |||
| 906 | for _, p := range sc.mon.Layout() { | 918 | for _, p := range sc.mon.Layout() { | ||
| 907 | if p.Hidden { | 919 | if p.Hidden { | ||
| 908 | continue | 920 | continue | ||
| 909 | } | 921 | } | ||
| 910 | rm := sc.rects[p.ID] | 922 | rm := sc.rects[p.ID] | ||
| 911 | 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 }) | ||
| 924 | + | if i >= 0 && at >= 0 && wrapped(w.stashIndex(p.ID), b.card, at, n, b.wraps) { | |||
| 925 | + | // The card went round the carousel to its other end: it | |||
| 926 | + | // fades in there instead of crossing the row. | |||
| 927 | + | rm, i = rectMotion{}, -1 | |||
| 928 | + | } | |||
| 912 | if i >= 0 && changed { | 929 | if i >= 0 && changed { | ||
| 913 | c.retargetLevel(&rm.fade, &rm.df, b.rects[i].fade, b.rects[i].fadeV, now) | 930 | c.retargetLevel(&rm.fade, &rm.df, b.rects[i].fade, b.rects[i].fadeV, now) | ||
| 914 | } | 931 | } | ||
| @@ -934,6 +951,26 @@ func (c *Core) transitionOverview(b *viewShot, now time.Time) { | |||||
| 934 | } | 951 | } | ||
| 935 | } | 952 | } | ||
| 936 | 953 | ||||
| 954 | + | // carouselStep is the selected card of the stash carousel b's screen shows | |||
| 955 | + | // now, when it showed the same carousel in b too (the same workspace, stash | |||
| 956 | + | // size and wrapping); else -1. | |||
| 957 | + | func carouselStep(b *viewShot) int { | |||
| 958 | + | m := b.sc.mon | |||
| 959 | + | w := m.Current() | |||
| 960 | + | if b.card < 0 || w != b.ws || len(w.Stash) != b.stashLen || w.wrapsCards() != b.wraps { | |||
| 961 | + | return -1 | |||
| 962 | + | } | |||
| 963 | + | return m.cardAt(w) | |||
| 964 | + | } | |||
| 965 | + | ||||
| 966 | + | // wrapped reports whether stash card i of a carousel of n cards went round | |||
| 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). | |||
| 970 | + | func wrapped(i, from, to, n int, wraps bool) bool { | |||
| 971 | + | return i >= 0 && carouselSlot(i, to, n, wraps)-carouselSlot(i, from, n, wraps) != carouselSlot(from, to, n, wraps) | |||
| 972 | + | } | |||
| 973 | + | ||||
| 937 | // 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 | ||
| 938 | // screen shows now (moved to it by an action), or nil. | 975 | // screen shows now (moved to it by an action), or nil. | ||
| 939 | func movedFrom(b *viewShot, before []viewShot) *viewShot { | 976 | func movedFrom(b *viewShot, before []viewShot) *viewShot { | ||
M internal/core/transition_stash_internal_test.go +55 -0
| @@ -473,3 +473,58 @@ func TestWithLeavingOrderIsDeterministic(t *testing.T) { | |||
| 473 | 473 | } | |
| 474 | 474 | } | |
| 475 | 475 | } | |
| 476 | + | ||
| 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. | |
| 481 | + | func TestCarouselWrapFadesIn(t *testing.T) { | |
| 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) | |
| 505 | + | } | |
| 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 | + | }) | |
| 529 | + | } | |
| 530 | + | } | |
| @@ -473,3 +473,58 @@ func TestWithLeavingOrderIsDeterministic(t *testing.T) { | |||||
| 473 | } | 473 | } | ||
| 474 | } | 474 | } | ||
| 475 | } | 475 | } | ||
| 476 | + | ||||
| 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. | |||
| 481 | + | func TestCarouselWrapFadesIn(t *testing.T) { | |||
| 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) | |||
| 505 | + | } | |||
| 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 | + | }) | |||
| 529 | + | } | |||
| 530 | + | } | |||