feat(overview): unfold the selected stash pile
The overview showed a stash as a folded pile even when selected, so the stashed windows behind the front card were hard to see and count. The pile now unfolds when selected into a strip of every card: a row under the lanes in a cascade overview, a column left of the row otherwise. Moves along the strip pick a card, and a strip longer than the area scrolls to keep the selection in view. Leaving it folds the pile again. In a cascade overview the pile now sits under the selected lane instead of over its bottom edge: the lane and the pile are centred together.
- commit
- fc79ddfb17dadbbe018cc88f96a52b66f9fb7412
- author
- brice · · 2026-10-15 05:16 UTC
- parent
- 8193e80
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +394 -112
M docs/config.md +5 -1
| @@ -275,4 +275,8 @@ The window must appear within 5 seconds, otherwise it opens normally. Apps that | |||
| 275 | 275 | ||
| 276 | 276 | `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 | 277 | ||
| 278 | - | With `layout.overflow = cascade`, workspaces sit side by side and each band of columns is a lane. Down (or fingers down on the touchpad, with natural scroll off) moves to the lane below; past the last lane it selects the stash pile under it, and down again brings the next stash card to the front. Up from a stash card leaves the pile for the last lane, at the column you came from. `focus-workspace-prev/next` also walk the pile in both directions, and left/right leave it. | |
| 278 | + | 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 | + | ||
| 280 | + | 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 | + | ||
| 282 | + | Otherwise, left from the first column unfolds the stash. Up/down walk the cards and right goes back to the columns. | |
| @@ -275,4 +275,8 @@ The window must appear within 5 seconds, otherwise it opens normally. Apps that | |||||
| 275 | 275 | ||||
| 276 | `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. | 276 | `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 | 277 | ||||
| 278 | - | With `layout.overflow = cascade`, workspaces sit side by side and each band of columns is a lane. Down (or fingers down on the touchpad, with natural scroll off) moves to the lane below; past the last lane it selects the stash pile under it, and down again brings the next stash card to the front. Up from a stash card leaves the pile for the last lane, at the column you came from. `focus-workspace-prev/next` also walk the pile in both directions, and left/right leave it. | 278 | + | 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 | + | ||||
| 280 | + | 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 | + | ||||
| 282 | + | Otherwise, left from the first column unfolds the stash. Up/down walk the cards and right goes back to the columns. | |||
M internal/core/overview.go +141 -71
| @@ -53,12 +53,15 @@ type rowSnapshot struct { | |||
| 53 | 53 | // workspace. A pinned fullscreen window is the front card of its row, the | |
| 54 | 54 | // windows it hides behind it; choosing one of them leaves fullscreen. | |
| 55 | 55 | ||
| 56 | - | // A workspace's stash shows as a pile of cards: the selected stashed window | |
| 57 | - | // in front at overviewCardZoom, up to overviewCards-1 others behind it, each | |
| 58 | - | // overviewCardStep further up and right, dimmed. A vertical overview pins | |
| 59 | - | // the pile to the left edge of its row, which takes the width the pile | |
| 60 | - | // leaves. A horizontal one centres it on its row, straddling the bottom edge | |
| 61 | - | // of the selected lane, over the tiles. | |
| 56 | + | // A workspace's stash shows as a pile of cards, folded or unfolded. Folded | |
| 57 | + | // (not selected), the stash's own selection is in front at | |
| 58 | + | // overviewCardZoom, up to overviewCards-1 others behind it, each | |
| 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 | |
| 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, straddling the | |
| 64 | + | // bottom edge of the selected lane, over the tiles. | |
| 62 | 65 | const ( | |
| 63 | 66 | overviewCardZoom = 0.2 | |
| 64 | 67 | overviewCards = 4 | |
| @@ -398,21 +401,20 @@ func (m *Monitor) OverviewMove(dx, dy int) { | |||
| 398 | 401 | } | |
| 399 | 402 | ||
| 400 | 403 | // overviewWorkspaceStep is a focus-workspace bind. A vertical overview | |
| 401 | - | // walks the stack cards before changing workspace (overviewVertical). A | |
| 402 | - | // side-by-side one always changes workspace, since its stack fans left and | |
| 403 | - | // right. A selected stash card cycles the pile first, in every layout. | |
| 404 | + | // walks the unfolded stash and the stack cards before changing workspace | |
| 405 | + | // (overviewVertical). A side-by-side one always changes workspace, since | |
| 406 | + | // its stack and unfolded stash run left and right. | |
| 404 | 407 | func (m *Monitor) overviewWorkspaceStep(d int) { | |
| 405 | 408 | if m.overviewAxis() == verticalAxis { | |
| 406 | 409 | m.overviewVertical(d) | |
| 407 | 410 | return | |
| 408 | 411 | } | |
| 409 | - | if !m.cycleCard(m.Current(), d) { | |
| 410 | - | m.overviewStep(d) | |
| 411 | - | } | |
| 412 | + | m.overviewStep(d) | |
| 412 | 413 | } | |
| 413 | 414 | ||
| 414 | - | // cycleCard brings the stash card d entries from the selected one of w to | |
| 415 | - | // the front, stopping at either end. It reports whether a card was selected. | |
| 415 | + | // cycleCard selects the stash card d entries from the selected one of w in | |
| 416 | + | // the unfolded pile, stopping at either end. It reports whether a card was | |
| 417 | + | // selected. | |
| 416 | 418 | func (m *Monitor) cycleCard(w *Workspace, d int) bool { | |
| 417 | 419 | at := m.cardAt(w) | |
| 418 | 420 | if at < 0 { | |
| @@ -423,19 +425,16 @@ func (m *Monitor) cycleCard(w *Workspace, d int) bool { | |||
| 423 | 425 | } | |
| 424 | 426 | ||
| 425 | 427 | // overviewLane is the vertical move of an overview whose workspaces sit side | |
| 426 | - | // by side (cascade). From a selected stash card, down brings the next card | |
| 427 | - | // of the pile to the front (stopping at the last) and up leaves the pile for | |
| 428 | - | // the last lane (leavePile); focus-workspace-prev/next cycle it both ways. | |
| 429 | - | // Else the selection goes to the previous or next lane (band), at the | |
| 430 | - | // closest column, stopping at the first. Down past the last lane, or from a | |
| 431 | - | // card that is not the columns, enters the stash, which sits below. Covering | |
| 432 | - | // floats stack along the horizontal axis, not this one. | |
| 428 | + | // by side (cascade). From the unfolded stash, up leaves it for the last lane | |
| 429 | + | // (leavePile) and down stays: the stash is the bottom. Else the selection | |
| 430 | + | // goes to the previous or next lane (band), at the closest column, stopping | |
| 431 | + | // at the first. Down past the last lane, or from a card that is not the | |
| 432 | + | // columns, enters the stash, which sits below. Covering floats stack along | |
| 433 | + | // the horizontal axis, not this one. | |
| 433 | 434 | func (m *Monitor) overviewLane(dy int) { | |
| 434 | 435 | w := m.Current() | |
| 435 | 436 | if m.cardAt(w) >= 0 { | |
| 436 | - | if dy > 0 { | |
| 437 | - | m.cycleCard(w, dy) | |
| 438 | - | } else { | |
| 437 | + | if dy < 0 { | |
| 439 | 438 | m.leavePile(w) | |
| 440 | 439 | } | |
| 441 | 440 | return | |
| @@ -479,31 +478,19 @@ func (m *Monitor) selectLaneColumn(w *Workspace, i int) { | |||
| 479 | 478 | } | |
| 480 | 479 | ||
| 481 | 480 | // overviewAcross is the horizontal move of an overview whose workspaces sit | |
| 482 | - | // side by side (cascade): a column inside the selected lane. Past the | |
| 483 | - | // lane's first column it goes to the previous workspace; past the last | |
| 484 | - | // column, to the next workspace. From a stash card, left goes to the | |
| 485 | - | // previous workspace and right back to the columns. Covering floats fan | |
| 481 | + | // side by side (cascade): a column inside the selected lane, or a card of | |
| 482 | + | // the unfolded stash. Past the first column or card it goes to the previous | |
| 483 | + | // workspace; past the last, to the next workspace. Covering floats fan | |
| 486 | 484 | // along this axis and are traversed before a workspace: the card on the | |
| 487 | 485 | // left is the one behind. | |
| 488 | 486 | func (m *Monitor) overviewAcross(dx int) { | |
| 489 | 487 | w := m.Current() | |
| 490 | - | if m.cardAt(w) >= 0 { | |
| 491 | - | if dx < 0 || len(w.stack()) == 0 { | |
| 492 | - | // Left leaves the pile; a stash-only workspace has nothing | |
| 493 | - | // to its right to select. | |
| 494 | - | m.overviewStep(dx) | |
| 488 | + | if at := m.cardAt(w); at >= 0 { | |
| 489 | + | if next := at + dx; next >= 0 && next < len(w.Stash) { | |
| 490 | + | m.selectCard(w, next) | |
| 495 | 491 | return | |
| 496 | 492 | } | |
| 497 | - | m.clearCard() | |
| 498 | - | m.ov.selected = 0 | |
| 499 | - | if len(w.Columns) > 0 { | |
| 500 | - | // The columns come to the front, even when down entered the | |
| 501 | - | // stash from a covering float: at the first column of the | |
| 502 | - | // focused lane. | |
| 503 | - | m.setFront(w, stackItem{kind: stackColumns}) | |
| 504 | - | i, _ := w.laneBounds(w.laneOf(w.Focus)) | |
| 505 | - | m.selectLaneColumn(w, i) | |
| 506 | - | } | |
| 493 | + | m.overviewStep(dx) | |
| 507 | 494 | return | |
| 508 | 495 | } | |
| 509 | 496 | if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 { | |
| @@ -744,13 +731,23 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement { | |||
| 744 | 731 | cur := m.Current() | |
| 745 | 732 | rows, _ := m.overviewRows() | |
| 746 | 733 | axis := m.overviewAxis() | |
| 734 | + | // The unfolded stash goes last: its strip may run over the neighbor | |
| 735 | + | // rows, which must not cover it. | |
| 736 | + | var unfolded []Placement | |
| 747 | 737 | for w := range m.all() { | |
| 748 | 738 | slot, shown := rows[w] | |
| 749 | 739 | if shown { | |
| 740 | + | // The pile band goes under the row: the row is centred | |
| 741 | + | // with it. | |
| 742 | + | slot.H -= w.pileBand(axis) | |
| 750 | 743 | at := m.cardAt(w) | |
| 751 | 744 | result = append(result, m.stackRow(w, axis, slot, w != cur, at < 0)...) | |
| 752 | 745 | if len(w.Stash) > 0 { | |
| 753 | - | result = append(result, w.pile(axis, slot, w != cur, at)...) | |
| 746 | + | if cards := w.pile(axis, slot, w != cur, at); at >= 0 { | |
| 747 | + | unfolded = cards | |
| 748 | + | } else { | |
| 749 | + | result = append(result, cards...) | |
| 750 | + | } | |
| 754 | 751 | } | |
| 755 | 752 | } | |
| 756 | 753 | for _, f := range w.Floats { | |
| @@ -763,7 +760,7 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement { | |||
| 763 | 760 | result = w.appendHidden(result, false) | |
| 764 | 761 | } | |
| 765 | 762 | } | |
| 766 | - | return result | |
| 763 | + | return append(result, unfolded...) | |
| 767 | 764 | } | |
| 768 | 765 | ||
| 769 | 766 | // fan places front and neighboring cards, farthest visible first and front | |
| @@ -846,6 +843,24 @@ func (w *Workspace) pileWidth() int { | |||
| 846 | 843 | return min(card+(min(len(w.Stash), overviewCards)-1)*w.cardStep()+a.W*2/100, a.W/3) | |
| 847 | 844 | } | |
| 848 | 845 | ||
| 846 | + | // cardSize is the size of a stash card in the overview. | |
| 847 | + | func (w *Workspace) cardSize() (int, int) { | |
| 848 | + | r := w.stashRect() | |
| 849 | + | return int(math.Round(float64(r.W) * overviewCardZoom)), int(math.Round(float64(r.H) * overviewCardZoom)) | |
| 850 | + | } | |
| 851 | + | ||
| 852 | + | // pileBand is the height a horizontal overview gives the stash of w under | |
| 853 | + | // its selected lane: the spacing, then the folded pile with its cards | |
| 854 | + | // behind. 0 without a stash, or in a vertical overview, where the pile | |
| 855 | + | // sits left of the row. The lane and the band are centred together. | |
| 856 | + | func (w *Workspace) pileBand(axis layoutAxis) int { | |
| 857 | + | if axis != horizontalAxis || len(w.Stash) == 0 { | |
| 858 | + | return 0 | |
| 859 | + | } | |
| 860 | + | _, ch := w.cardSize() | |
| 861 | + | return max(1, w.overviewArea().H*3/100) + ch + (min(len(w.Stash), overviewCards)-1)*w.cardStep() | |
| 862 | + | } | |
| 863 | + | ||
| 849 | 864 | // cardStep is the offset between stacked cards, smaller on tiny outputs. | |
| 850 | 865 | func (w *Workspace) cardStep() int { | |
| 851 | 866 | a := w.overviewArea() | |
| @@ -853,46 +868,87 @@ func (w *Workspace) cardStep() int { | |||
| 853 | 868 | } | |
| 854 | 869 | ||
| 855 | 870 | // pile places the stash of w as cards in its slot along axis (see the | |
| 856 | - | // overview's stash comment): entry front (the stash selection when -1) in | |
| 857 | - | // front, the next ones behind it, dimmed, the rest hidden. Only a front card | |
| 858 | - | // of the current row (dim false) with front set is selected. | |
| 859 | - | func (w *Workspace) pile(axis layoutAxis, slot Rect, dim bool, front int) []Placement { | |
| 871 | + | // overview's stash comment). With entry at selected (at >= 0, on the | |
| 872 | + | // current row: dim false) the pile unfolds; else it is folded, the stash | |
| 873 | + | // selection in front, the next ones behind it, dimmed, the rest hidden. | |
| 874 | + | func (w *Workspace) pile(axis layoutAxis, slot Rect, dim bool, at int) []Placement { | |
| 860 | 875 | n := len(w.Stash) | |
| 861 | 876 | if n == 0 { | |
| 862 | 877 | return nil | |
| 863 | 878 | } | |
| 864 | - | selected := front >= 0 && !dim | |
| 865 | - | if front < 0 { | |
| 866 | - | front = w.stashAt | |
| 867 | - | } | |
| 868 | - | front = min(max(front, 0), n-1) | |
| 869 | - | r := w.stashRect() | |
| 870 | 879 | u := w.overviewArea() | |
| 871 | - | cw := int(math.Round(float64(r.W) * overviewCardZoom)) | |
| 872 | - | ch := int(math.Round(float64(r.H) * overviewCardZoom)) | |
| 880 | + | cw, ch := w.cardSize() | |
| 873 | 881 | rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom(axis))) | |
| 874 | 882 | var x0, y0 int | |
| 875 | 883 | if axis == horizontalAxis { | |
| 876 | - | // Centred on the row, across the bottom edge of the selected | |
| 877 | - | // lane, which is centred in the slot. Both scale with the area | |
| 878 | - | // (the lane at most overviewMaxZoom of it), so the pile stays | |
| 879 | - | // inside whatever its size. | |
| 884 | + | // Centred on the row, under the selected lane: slot is the | |
| 885 | + | // lane's, the pile band (pileBand) taken off its height, so the | |
| 886 | + | // band starts below the lane. The cards behind rise from the | |
| 887 | + | // front one, which goes at the bottom of the band. Both scale | |
| 888 | + | // with the area (the lane at most overviewMaxZoom of it), so | |
| 889 | + | // the pile stays inside whatever its size. | |
| 880 | 890 | x0 = slot.X + (slot.W-cw)/2 | |
| 881 | - | y0 = slot.Y + (slot.H-rowH)/2 + rowH - ch/2 | |
| 891 | + | y0 = slot.Y + (slot.H-rowH)/2 + rowH + w.pileBand(axis) - ch | |
| 882 | 892 | } else { | |
| 883 | 893 | // Stack rows reserve two peek steps above the front card. Align | |
| 884 | 894 | // the stash with that front band, not with the top of its envelope. | |
| 885 | 895 | band := w.frontSlot(axis, slot) | |
| 886 | 896 | x0, y0 = band.X+u.W/100, band.Y+(rowH-ch)/2 | |
| 887 | 897 | } | |
| 898 | + | folded := Rect{X: x0, Y: y0, W: cw, H: ch} | |
| 899 | + | if at >= 0 && !dim { | |
| 900 | + | return w.unfold(axis, folded, min(at, n-1)) | |
| 901 | + | } | |
| 888 | 902 | cards := make([][]Placement, n) | |
| 889 | 903 | for i := range w.Stash { | |
| 890 | - | cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: Rect{X: x0, Y: y0, W: cw, H: ch}, Preview: overviewCardZoom, Focused: selected, Card: true}} | |
| 904 | + | cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: folded, Preview: overviewCardZoom, Card: true}} | |
| 891 | 905 | } | |
| 892 | 906 | step := w.cardStep() | |
| 893 | - | return fan(cards, front, min(n, overviewCards)-1, 0, true, | |
| 907 | + | return fan(cards, min(max(w.stashAt, 0), n-1), min(n, overviewCards)-1, 0, true, | |
| 894 | 908 | func(d int) (int, int) { return d * step, -d * step }, | |
| 895 | - | func(int) (int, int) { return 0, 0 }, dim, selected) | |
| 909 | + | func(int) (int, int) { return 0, 0 }, dim, false) | |
| 910 | + | } | |
| 911 | + | ||
| 912 | + | // unfold lays the stash of w out as a strip of every card along axis, from | |
| 913 | + | // the folded pile's card rect: a row in a horizontal overview, a column in | |
| 914 | + | // a vertical one. The strip is centred on the pile and kept in the overview | |
| 915 | + | // area. Longer than the area, it scrolls like a wide row: the selected card | |
| 916 | + | // at is centred, the strip stopping at the area's edges. The other cards | |
| 917 | + | // are dimmed; those out of the area are hidden. | |
| 918 | + | func (w *Workspace) unfold(axis layoutAxis, folded Rect, at int) []Placement { | |
| 919 | + | n := len(w.Stash) | |
| 920 | + | u := w.overviewArea() | |
| 921 | + | size, origin, lo, hi := folded.W, folded.X, u.X, u.X+u.W | |
| 922 | + | if axis == verticalAxis { | |
| 923 | + | size, origin, lo, hi = folded.H, folded.Y, u.Y, u.Y+u.H | |
| 924 | + | } | |
| 925 | + | stride := size + 2*w.cardStep() | |
| 926 | + | total := n*stride - (stride - size) | |
| 927 | + | start := origin + size/2 - total/2 | |
| 928 | + | if total > hi-lo { | |
| 929 | + | start = origin + size/2 - at*stride - size/2 | |
| 930 | + | start = min(max(start, hi-total), lo) | |
| 931 | + | } else { | |
| 932 | + | start = min(max(start, lo), hi-total) | |
| 933 | + | } | |
| 934 | + | out := make([]Placement, n) | |
| 935 | + | for i := range w.Stash { | |
| 936 | + | r := folded | |
| 937 | + | if axis == verticalAxis { | |
| 938 | + | r.Y = start + i*stride | |
| 939 | + | } else { | |
| 940 | + | r.X = start + i*stride | |
| 941 | + | } | |
| 942 | + | p := Placement{ID: w.Stash[i].ID, Rect: r, Preview: overviewCardZoom, Card: true, Focused: i == at} | |
| 943 | + | if i != at { | |
| 944 | + | p.peeking(1) | |
| 945 | + | } | |
| 946 | + | if !r.Overlaps(u) { | |
| 947 | + | p.Hidden, p.Preview, p.Focused = true, 0, false | |
| 948 | + | } | |
| 949 | + | out[i] = p | |
| 950 | + | } | |
| 951 | + | return out | |
| 896 | 952 | } | |
| 897 | 953 | ||
| 898 | 954 | // overviewZoom is the scale that fits every column of w in the usable | |
| @@ -1098,6 +1154,8 @@ func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes | |||
| 1098 | 1154 | // Lanes along the overview axis would overlap the neighbor workspaces. | |
| 1099 | 1155 | otherLanes := w.policy().content != axis | |
| 1100 | 1156 | laneStep := scale(stride) + max(1, u.H*3/100) | |
| 1157 | + | // The lanes below the selected one leave room for the stash pile. | |
| 1158 | + | below := w.pileBand(axis) | |
| 1101 | 1159 | selLane := 0 | |
| 1102 | 1160 | if lanes { | |
| 1103 | 1161 | selLane = max(sel.Y, 0) / stride | |
| @@ -1113,7 +1171,11 @@ func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes | |||
| 1113 | 1171 | lane, ly = l-selLane, r.Y-l*stride | |
| 1114 | 1172 | } | |
| 1115 | 1173 | x0, y0 := scale(r.X), scale(ly) | |
| 1116 | - | tiles[i].Rect = Rect{X: x + x0, Y: top + y0 + lane*laneStep, W: scale(r.X+r.W) - x0, H: scale(ly+r.H) - y0} | |
| 1174 | + | y := top + y0 + lane*laneStep | |
| 1175 | + | if lanes && lane > 0 { | |
| 1176 | + | y += below | |
| 1177 | + | } | |
| 1178 | + | tiles[i].Rect = Rect{X: x + x0, Y: y, W: scale(r.X+r.W) - x0, H: scale(ly+r.H) - y0} | |
| 1117 | 1179 | tiles[i].Preview = z | |
| 1118 | 1180 | if dim || lane != 0 { | |
| 1119 | 1181 | tiles[i].peeking(1) | |
| @@ -1408,9 +1470,13 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||
| 1408 | 1470 | if finger { | |
| 1409 | 1471 | before = m.overviewSelection() | |
| 1410 | 1472 | } | |
| 1411 | - | if cascade && !finger && m.cardAt(m.Current()) < 0 { | |
| 1473 | + | switch { | |
| 1474 | + | case cascade && !finger && m.cardAt(m.Current()) >= 0: | |
| 1475 | + | // A wheel walks the unfolded stash, a row. | |
| 1476 | + | m.OverviewMove(d, 0) | |
| 1477 | + | case cascade && !finger: | |
| 1412 | 1478 | m.overviewStep(d) | |
| 1413 | - | } else { | |
| 1479 | + | default: | |
| 1414 | 1480 | m.OverviewMove(0, d) | |
| 1415 | 1481 | } | |
| 1416 | 1482 | if m.overviewTurned(axis, finger) { | |
| @@ -1424,9 +1490,13 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||
| 1424 | 1490 | d := sign(m.ov.scrollX) | |
| 1425 | 1491 | if cascade && finger { | |
| 1426 | 1492 | before = m.overviewSelection() | |
| 1427 | - | // One step per gesture: one workspace, even over a | |
| 1428 | - | // stash card. | |
| 1429 | - | m.overviewStep(d) | |
| 1493 | + | // One step per gesture: one card of the unfolded | |
| 1494 | + | // stash, else one workspace. | |
| 1495 | + | if m.cardAt(m.Current()) >= 0 { | |
| 1496 | + | m.OverviewMove(d, 0) | |
| 1497 | + | } else { | |
| 1498 | + | m.overviewStep(d) | |
| 1499 | + | } | |
| 1430 | 1500 | m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true | |
| 1431 | 1501 | return m.overviewSelection() != before | |
| 1432 | 1502 | } | |
| @@ -53,12 +53,15 @@ type rowSnapshot struct { | |||||
| 53 | // workspace. A pinned fullscreen window is the front card of its row, the | 53 | // workspace. A pinned fullscreen window is the front card of its row, the | ||
| 54 | // windows it hides behind it; choosing one of them leaves fullscreen. | 54 | // windows it hides behind it; choosing one of them leaves fullscreen. | ||
| 55 | 55 | ||||
| 56 | - | // A workspace's stash shows as a pile of cards: the selected stashed window | 56 | + | // A workspace's stash shows as a pile of cards, folded or unfolded. Folded |
| 57 | - | // in front at overviewCardZoom, up to overviewCards-1 others behind it, each | 57 | + | // (not selected), the stash's own selection is in front at |
| 58 | - | // overviewCardStep further up and right, dimmed. A vertical overview pins | 58 | + | // overviewCardZoom, up to overviewCards-1 others behind it, each |
| 59 | - | // the pile to the left edge of its row, which takes the width the pile | 59 | + | // overviewCardStep further up and right, dimmed. Selected, the pile |
| 60 | - | // leaves. A horizontal one centres it on its row, straddling the bottom edge | 60 | + | // unfolds along the overview axis into a strip of every card (unfold), the |
| 61 | - | // of the selected lane, over the tiles. | 61 | + | // selection framed, and the moves along that axis walk it. A vertical |
| 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, straddling the | |||
| 64 | + | // bottom edge of the selected lane, over the tiles. | |||
| 62 | const ( | 65 | const ( | ||
| 63 | overviewCardZoom = 0.2 | 66 | overviewCardZoom = 0.2 | ||
| 64 | overviewCards = 4 | 67 | overviewCards = 4 | ||
| @@ -398,21 +401,20 @@ func (m *Monitor) OverviewMove(dx, dy int) { | |||||
| 398 | } | 401 | } | ||
| 399 | 402 | ||||
| 400 | // overviewWorkspaceStep is a focus-workspace bind. A vertical overview | 403 | // overviewWorkspaceStep is a focus-workspace bind. A vertical overview | ||
| 401 | - | // walks the stack cards before changing workspace (overviewVertical). A | 404 | + | // walks the unfolded stash and the stack cards before changing workspace |
| 402 | - | // side-by-side one always changes workspace, since its stack fans left and | 405 | + | // (overviewVertical). A side-by-side one always changes workspace, since |
| 403 | - | // right. A selected stash card cycles the pile first, in every layout. | 406 | + | // its stack and unfolded stash run left and right. |
| 404 | func (m *Monitor) overviewWorkspaceStep(d int) { | 407 | func (m *Monitor) overviewWorkspaceStep(d int) { | ||
| 405 | if m.overviewAxis() == verticalAxis { | 408 | if m.overviewAxis() == verticalAxis { | ||
| 406 | m.overviewVertical(d) | 409 | m.overviewVertical(d) | ||
| 407 | return | 410 | return | ||
| 408 | } | 411 | } | ||
| 409 | - | if !m.cycleCard(m.Current(), d) { | 412 | + | m.overviewStep(d) |
| 410 | - | m.overviewStep(d) | |||
| 411 | - | } | |||
| 412 | } | 413 | } | ||
| 413 | 414 | ||||
| 414 | - | // cycleCard brings the stash card d entries from the selected one of w to | 415 | + | // cycleCard selects the stash card d entries from the selected one of w in |
| 415 | - | // the front, stopping at either end. It reports whether a card was selected. | 416 | + | // the unfolded pile, stopping at either end. It reports whether a card was |
| 417 | + | // selected. | |||
| 416 | func (m *Monitor) cycleCard(w *Workspace, d int) bool { | 418 | func (m *Monitor) cycleCard(w *Workspace, d int) bool { | ||
| 417 | at := m.cardAt(w) | 419 | at := m.cardAt(w) | ||
| 418 | if at < 0 { | 420 | if at < 0 { | ||
| @@ -423,19 +425,16 @@ func (m *Monitor) cycleCard(w *Workspace, d int) bool { | |||||
| 423 | } | 425 | } | ||
| 424 | 426 | ||||
| 425 | // overviewLane is the vertical move of an overview whose workspaces sit side | 427 | // overviewLane is the vertical move of an overview whose workspaces sit side | ||
| 426 | - | // by side (cascade). From a selected stash card, down brings the next card | 428 | + | // by side (cascade). From the unfolded stash, up leaves it for the last lane |
| 427 | - | // of the pile to the front (stopping at the last) and up leaves the pile for | 429 | + | // (leavePile) and down stays: the stash is the bottom. Else the selection |
| 428 | - | // the last lane (leavePile); focus-workspace-prev/next cycle it both ways. | 430 | + | // goes to the previous or next lane (band), at the closest column, stopping |
| 429 | - | // Else the selection goes to the previous or next lane (band), at the | 431 | + | // at the first. Down past the last lane, or from a card that is not the |
| 430 | - | // closest column, stopping at the first. Down past the last lane, or from a | 432 | + | // columns, enters the stash, which sits below. Covering floats stack along |
| 431 | - | // card that is not the columns, enters the stash, which sits below. Covering | 433 | + | // the horizontal axis, not this one. |
| 432 | - | // floats stack along the horizontal axis, not this one. | |||
| 433 | func (m *Monitor) overviewLane(dy int) { | 434 | func (m *Monitor) overviewLane(dy int) { | ||
| 434 | w := m.Current() | 435 | w := m.Current() | ||
| 435 | if m.cardAt(w) >= 0 { | 436 | if m.cardAt(w) >= 0 { | ||
| 436 | - | if dy > 0 { | 437 | + | if dy < 0 { |
| 437 | - | m.cycleCard(w, dy) | |||
| 438 | - | } else { | |||
| 439 | m.leavePile(w) | 438 | m.leavePile(w) | ||
| 440 | } | 439 | } | ||
| 441 | return | 440 | return | ||
| @@ -479,31 +478,19 @@ func (m *Monitor) selectLaneColumn(w *Workspace, i int) { | |||||
| 479 | } | 478 | } | ||
| 480 | 479 | ||||
| 481 | // overviewAcross is the horizontal move of an overview whose workspaces sit | 480 | // overviewAcross is the horizontal move of an overview whose workspaces sit | ||
| 482 | - | // side by side (cascade): a column inside the selected lane. Past the | 481 | + | // side by side (cascade): a column inside the selected lane, or a card of |
| 483 | - | // lane's first column it goes to the previous workspace; past the last | 482 | + | // the unfolded stash. Past the first column or card it goes to the previous |
| 484 | - | // column, to the next workspace. From a stash card, left goes to the | 483 | + | // workspace; past the last, to the next workspace. Covering floats fan |
| 485 | - | // previous workspace and right back to the columns. Covering floats fan | |||
| 486 | // along this axis and are traversed before a workspace: the card on the | 484 | // along this axis and are traversed before a workspace: the card on the | ||
| 487 | // left is the one behind. | 485 | // left is the one behind. | ||
| 488 | func (m *Monitor) overviewAcross(dx int) { | 486 | func (m *Monitor) overviewAcross(dx int) { | ||
| 489 | w := m.Current() | 487 | w := m.Current() | ||
| 490 | - | if m.cardAt(w) >= 0 { | 488 | + | if at := m.cardAt(w); at >= 0 { |
| 491 | - | if dx < 0 || len(w.stack()) == 0 { | 489 | + | if next := at + dx; next >= 0 && next < len(w.Stash) { |
| 492 | - | // Left leaves the pile; a stash-only workspace has nothing | 490 | + | m.selectCard(w, next) |
| 493 | - | // to its right to select. | |||
| 494 | - | m.overviewStep(dx) | |||
| 495 | return | 491 | return | ||
| 496 | } | 492 | } | ||
| 497 | - | m.clearCard() | 493 | + | m.overviewStep(dx) |
| 498 | - | m.ov.selected = 0 | |||
| 499 | - | if len(w.Columns) > 0 { | |||
| 500 | - | // The columns come to the front, even when down entered the | |||
| 501 | - | // stash from a covering float: at the first column of the | |||
| 502 | - | // focused lane. | |||
| 503 | - | m.setFront(w, stackItem{kind: stackColumns}) | |||
| 504 | - | i, _ := w.laneBounds(w.laneOf(w.Focus)) | |||
| 505 | - | m.selectLaneColumn(w, i) | |||
| 506 | - | } | |||
| 507 | return | 494 | return | ||
| 508 | } | 495 | } | ||
| 509 | if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 { | 496 | if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 { | ||
| @@ -744,13 +731,23 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement { | |||||
| 744 | cur := m.Current() | 731 | cur := m.Current() | ||
| 745 | rows, _ := m.overviewRows() | 732 | rows, _ := m.overviewRows() | ||
| 746 | axis := m.overviewAxis() | 733 | axis := m.overviewAxis() | ||
| 734 | + | // The unfolded stash goes last: its strip may run over the neighbor | |||
| 735 | + | // rows, which must not cover it. | |||
| 736 | + | var unfolded []Placement | |||
| 747 | for w := range m.all() { | 737 | for w := range m.all() { | ||
| 748 | slot, shown := rows[w] | 738 | slot, shown := rows[w] | ||
| 749 | if shown { | 739 | if shown { | ||
| 740 | + | // The pile band goes under the row: the row is centred | |||
| 741 | + | // with it. | |||
| 742 | + | slot.H -= w.pileBand(axis) | |||
| 750 | at := m.cardAt(w) | 743 | at := m.cardAt(w) | ||
| 751 | result = append(result, m.stackRow(w, axis, slot, w != cur, at < 0)...) | 744 | result = append(result, m.stackRow(w, axis, slot, w != cur, at < 0)...) | ||
| 752 | if len(w.Stash) > 0 { | 745 | if len(w.Stash) > 0 { | ||
| 753 | - | result = append(result, w.pile(axis, slot, w != cur, at)...) | 746 | + | if cards := w.pile(axis, slot, w != cur, at); at >= 0 { |
| 747 | + | unfolded = cards | |||
| 748 | + | } else { | |||
| 749 | + | result = append(result, cards...) | |||
| 750 | + | } | |||
| 754 | } | 751 | } | ||
| 755 | } | 752 | } | ||
| 756 | for _, f := range w.Floats { | 753 | for _, f := range w.Floats { | ||
| @@ -763,7 +760,7 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement { | |||||
| 763 | result = w.appendHidden(result, false) | 760 | result = w.appendHidden(result, false) | ||
| 764 | } | 761 | } | ||
| 765 | } | 762 | } | ||
| 766 | - | return result | 763 | + | return append(result, unfolded...) |
| 767 | } | 764 | } | ||
| 768 | 765 | ||||
| 769 | // fan places front and neighboring cards, farthest visible first and front | 766 | // fan places front and neighboring cards, farthest visible first and front | ||
| @@ -846,6 +843,24 @@ func (w *Workspace) pileWidth() int { | |||||
| 846 | return min(card+(min(len(w.Stash), overviewCards)-1)*w.cardStep()+a.W*2/100, a.W/3) | 843 | return min(card+(min(len(w.Stash), overviewCards)-1)*w.cardStep()+a.W*2/100, a.W/3) | ||
| 847 | } | 844 | } | ||
| 848 | 845 | ||||
| 846 | + | // cardSize is the size of a stash card in the overview. | |||
| 847 | + | func (w *Workspace) cardSize() (int, int) { | |||
| 848 | + | r := w.stashRect() | |||
| 849 | + | return int(math.Round(float64(r.W) * overviewCardZoom)), int(math.Round(float64(r.H) * overviewCardZoom)) | |||
| 850 | + | } | |||
| 851 | + | ||||
| 852 | + | // pileBand is the height a horizontal overview gives the stash of w under | |||
| 853 | + | // its selected lane: the spacing, then the folded pile with its cards | |||
| 854 | + | // behind. 0 without a stash, or in a vertical overview, where the pile | |||
| 855 | + | // sits left of the row. The lane and the band are centred together. | |||
| 856 | + | func (w *Workspace) pileBand(axis layoutAxis) int { | |||
| 857 | + | if axis != horizontalAxis || len(w.Stash) == 0 { | |||
| 858 | + | return 0 | |||
| 859 | + | } | |||
| 860 | + | _, ch := w.cardSize() | |||
| 861 | + | return max(1, w.overviewArea().H*3/100) + ch + (min(len(w.Stash), overviewCards)-1)*w.cardStep() | |||
| 862 | + | } | |||
| 863 | + | ||||
| 849 | // cardStep is the offset between stacked cards, smaller on tiny outputs. | 864 | // cardStep is the offset between stacked cards, smaller on tiny outputs. | ||
| 850 | func (w *Workspace) cardStep() int { | 865 | func (w *Workspace) cardStep() int { | ||
| 851 | a := w.overviewArea() | 866 | a := w.overviewArea() | ||
| @@ -853,46 +868,87 @@ func (w *Workspace) cardStep() int { | |||||
| 853 | } | 868 | } | ||
| 854 | 869 | ||||
| 855 | // pile places the stash of w as cards in its slot along axis (see the | 870 | // pile places the stash of w as cards in its slot along axis (see the | ||
| 856 | - | // overview's stash comment): entry front (the stash selection when -1) in | 871 | + | // overview's stash comment). With entry at selected (at >= 0, on the |
| 857 | - | // front, the next ones behind it, dimmed, the rest hidden. Only a front card | 872 | + | // current row: dim false) the pile unfolds; else it is folded, the stash |
| 858 | - | // of the current row (dim false) with front set is selected. | 873 | + | // selection in front, the next ones behind it, dimmed, the rest hidden. |
| 859 | - | func (w *Workspace) pile(axis layoutAxis, slot Rect, dim bool, front int) []Placement { | 874 | + | func (w *Workspace) pile(axis layoutAxis, slot Rect, dim bool, at int) []Placement { |
| 860 | n := len(w.Stash) | 875 | n := len(w.Stash) | ||
| 861 | if n == 0 { | 876 | if n == 0 { | ||
| 862 | return nil | 877 | return nil | ||
| 863 | } | 878 | } | ||
| 864 | - | selected := front >= 0 && !dim | |||
| 865 | - | if front < 0 { | |||
| 866 | - | front = w.stashAt | |||
| 867 | - | } | |||
| 868 | - | front = min(max(front, 0), n-1) | |||
| 869 | - | r := w.stashRect() | |||
| 870 | u := w.overviewArea() | 879 | u := w.overviewArea() | ||
| 871 | - | cw := int(math.Round(float64(r.W) * overviewCardZoom)) | 880 | + | cw, ch := w.cardSize() |
| 872 | - | ch := int(math.Round(float64(r.H) * overviewCardZoom)) | |||
| 873 | rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom(axis))) | 881 | rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom(axis))) | ||
| 874 | var x0, y0 int | 882 | var x0, y0 int | ||
| 875 | if axis == horizontalAxis { | 883 | if axis == horizontalAxis { | ||
| 876 | - | // Centred on the row, across the bottom edge of the selected | 884 | + | // Centred on the row, under the selected lane: slot is the |
| 877 | - | // lane, which is centred in the slot. Both scale with the area | 885 | + | // lane's, the pile band (pileBand) taken off its height, so the |
| 878 | - | // (the lane at most overviewMaxZoom of it), so the pile stays | 886 | + | // band starts below the lane. The cards behind rise from the |
| 879 | - | // inside whatever its size. | 887 | + | // front one, which goes at the bottom of the band. Both scale |
| 888 | + | // with the area (the lane at most overviewMaxZoom of it), so | |||
| 889 | + | // the pile stays inside whatever its size. | |||
| 880 | x0 = slot.X + (slot.W-cw)/2 | 890 | x0 = slot.X + (slot.W-cw)/2 | ||
| 881 | - | y0 = slot.Y + (slot.H-rowH)/2 + rowH - ch/2 | 891 | + | y0 = slot.Y + (slot.H-rowH)/2 + rowH + w.pileBand(axis) - ch |
| 882 | } else { | 892 | } else { | ||
| 883 | // Stack rows reserve two peek steps above the front card. Align | 893 | // Stack rows reserve two peek steps above the front card. Align | ||
| 884 | // the stash with that front band, not with the top of its envelope. | 894 | // the stash with that front band, not with the top of its envelope. | ||
| 885 | band := w.frontSlot(axis, slot) | 895 | band := w.frontSlot(axis, slot) | ||
| 886 | x0, y0 = band.X+u.W/100, band.Y+(rowH-ch)/2 | 896 | x0, y0 = band.X+u.W/100, band.Y+(rowH-ch)/2 | ||
| 887 | } | 897 | } | ||
| 898 | + | folded := Rect{X: x0, Y: y0, W: cw, H: ch} | |||
| 899 | + | if at >= 0 && !dim { | |||
| 900 | + | return w.unfold(axis, folded, min(at, n-1)) | |||
| 901 | + | } | |||
| 888 | cards := make([][]Placement, n) | 902 | cards := make([][]Placement, n) | ||
| 889 | for i := range w.Stash { | 903 | for i := range w.Stash { | ||
| 890 | - | cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: Rect{X: x0, Y: y0, W: cw, H: ch}, Preview: overviewCardZoom, Focused: selected, Card: true}} | 904 | + | cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: folded, Preview: overviewCardZoom, Card: true}} |
| 891 | } | 905 | } | ||
| 892 | step := w.cardStep() | 906 | step := w.cardStep() | ||
| 893 | - | return fan(cards, front, min(n, overviewCards)-1, 0, true, | 907 | + | return fan(cards, min(max(w.stashAt, 0), n-1), min(n, overviewCards)-1, 0, true, |
| 894 | func(d int) (int, int) { return d * step, -d * step }, | 908 | func(d int) (int, int) { return d * step, -d * step }, | ||
| 895 | - | func(int) (int, int) { return 0, 0 }, dim, selected) | 909 | + | func(int) (int, int) { return 0, 0 }, dim, false) |
| 910 | + | } | |||
| 911 | + | ||||
| 912 | + | // unfold lays the stash of w out as a strip of every card along axis, from | |||
| 913 | + | // the folded pile's card rect: a row in a horizontal overview, a column in | |||
| 914 | + | // a vertical one. The strip is centred on the pile and kept in the overview | |||
| 915 | + | // area. Longer than the area, it scrolls like a wide row: the selected card | |||
| 916 | + | // at is centred, the strip stopping at the area's edges. The other cards | |||
| 917 | + | // are dimmed; those out of the area are hidden. | |||
| 918 | + | func (w *Workspace) unfold(axis layoutAxis, folded Rect, at int) []Placement { | |||
| 919 | + | n := len(w.Stash) | |||
| 920 | + | u := w.overviewArea() | |||
| 921 | + | size, origin, lo, hi := folded.W, folded.X, u.X, u.X+u.W | |||
| 922 | + | if axis == verticalAxis { | |||
| 923 | + | size, origin, lo, hi = folded.H, folded.Y, u.Y, u.Y+u.H | |||
| 924 | + | } | |||
| 925 | + | stride := size + 2*w.cardStep() | |||
| 926 | + | total := n*stride - (stride - size) | |||
| 927 | + | start := origin + size/2 - total/2 | |||
| 928 | + | if total > hi-lo { | |||
| 929 | + | start = origin + size/2 - at*stride - size/2 | |||
| 930 | + | start = min(max(start, hi-total), lo) | |||
| 931 | + | } else { | |||
| 932 | + | start = min(max(start, lo), hi-total) | |||
| 933 | + | } | |||
| 934 | + | out := make([]Placement, n) | |||
| 935 | + | for i := range w.Stash { | |||
| 936 | + | r := folded | |||
| 937 | + | if axis == verticalAxis { | |||
| 938 | + | r.Y = start + i*stride | |||
| 939 | + | } else { | |||
| 940 | + | r.X = start + i*stride | |||
| 941 | + | } | |||
| 942 | + | p := Placement{ID: w.Stash[i].ID, Rect: r, Preview: overviewCardZoom, Card: true, Focused: i == at} | |||
| 943 | + | if i != at { | |||
| 944 | + | p.peeking(1) | |||
| 945 | + | } | |||
| 946 | + | if !r.Overlaps(u) { | |||
| 947 | + | p.Hidden, p.Preview, p.Focused = true, 0, false | |||
| 948 | + | } | |||
| 949 | + | out[i] = p | |||
| 950 | + | } | |||
| 951 | + | return out | |||
| 896 | } | 952 | } | ||
| 897 | 953 | ||||
| 898 | // overviewZoom is the scale that fits every column of w in the usable | 954 | // overviewZoom is the scale that fits every column of w in the usable | ||
| @@ -1098,6 +1154,8 @@ func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes | |||||
| 1098 | // Lanes along the overview axis would overlap the neighbor workspaces. | 1154 | // Lanes along the overview axis would overlap the neighbor workspaces. | ||
| 1099 | otherLanes := w.policy().content != axis | 1155 | otherLanes := w.policy().content != axis | ||
| 1100 | laneStep := scale(stride) + max(1, u.H*3/100) | 1156 | laneStep := scale(stride) + max(1, u.H*3/100) | ||
| 1157 | + | // The lanes below the selected one leave room for the stash pile. | |||
| 1158 | + | below := w.pileBand(axis) | |||
| 1101 | selLane := 0 | 1159 | selLane := 0 | ||
| 1102 | if lanes { | 1160 | if lanes { | ||
| 1103 | selLane = max(sel.Y, 0) / stride | 1161 | selLane = max(sel.Y, 0) / stride | ||
| @@ -1113,7 +1171,11 @@ func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes | |||||
| 1113 | lane, ly = l-selLane, r.Y-l*stride | 1171 | lane, ly = l-selLane, r.Y-l*stride | ||
| 1114 | } | 1172 | } | ||
| 1115 | x0, y0 := scale(r.X), scale(ly) | 1173 | x0, y0 := scale(r.X), scale(ly) | ||
| 1116 | - | tiles[i].Rect = Rect{X: x + x0, Y: top + y0 + lane*laneStep, W: scale(r.X+r.W) - x0, H: scale(ly+r.H) - y0} | 1174 | + | y := top + y0 + lane*laneStep |
| 1175 | + | if lanes && lane > 0 { | |||
| 1176 | + | y += below | |||
| 1177 | + | } | |||
| 1178 | + | tiles[i].Rect = Rect{X: x + x0, Y: y, W: scale(r.X+r.W) - x0, H: scale(ly+r.H) - y0} | |||
| 1117 | tiles[i].Preview = z | 1179 | tiles[i].Preview = z | ||
| 1118 | if dim || lane != 0 { | 1180 | if dim || lane != 0 { | ||
| 1119 | tiles[i].peeking(1) | 1181 | tiles[i].peeking(1) | ||
| @@ -1408,9 +1470,13 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||||
| 1408 | if finger { | 1470 | if finger { | ||
| 1409 | before = m.overviewSelection() | 1471 | before = m.overviewSelection() | ||
| 1410 | } | 1472 | } | ||
| 1411 | - | if cascade && !finger && m.cardAt(m.Current()) < 0 { | 1473 | + | switch { |
| 1474 | + | case cascade && !finger && m.cardAt(m.Current()) >= 0: | |||
| 1475 | + | // A wheel walks the unfolded stash, a row. | |||
| 1476 | + | m.OverviewMove(d, 0) | |||
| 1477 | + | case cascade && !finger: | |||
| 1412 | m.overviewStep(d) | 1478 | m.overviewStep(d) | ||
| 1413 | - | } else { | 1479 | + | default: |
| 1414 | m.OverviewMove(0, d) | 1480 | m.OverviewMove(0, d) | ||
| 1415 | } | 1481 | } | ||
| 1416 | if m.overviewTurned(axis, finger) { | 1482 | if m.overviewTurned(axis, finger) { | ||
| @@ -1424,9 +1490,13 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||||
| 1424 | d := sign(m.ov.scrollX) | 1490 | d := sign(m.ov.scrollX) | ||
| 1425 | if cascade && finger { | 1491 | if cascade && finger { | ||
| 1426 | before = m.overviewSelection() | 1492 | before = m.overviewSelection() | ||
| 1427 | - | // One step per gesture: one workspace, even over a | 1493 | + | // One step per gesture: one card of the unfolded |
| 1428 | - | // stash card. | 1494 | + | // stash, else one workspace. |
| 1429 | - | m.overviewStep(d) | 1495 | + | if m.cardAt(m.Current()) >= 0 { |
| 1496 | + | m.OverviewMove(d, 0) | |||
| 1497 | + | } else { | |||
| 1498 | + | m.overviewStep(d) | |||
| 1499 | + | } | |||
| 1430 | m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true | 1500 | m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true | ||
| 1431 | return m.overviewSelection() != before | 1501 | return m.overviewSelection() != before | ||
| 1432 | } | 1502 | } | ||
M internal/core/overview_cascade_test.go +45 -40
| @@ -2,7 +2,6 @@ package core | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | - | "math" | |
| 6 | 5 | "slices" | |
| 7 | 6 | "testing" | |
| 8 | 7 | ||
| @@ -245,12 +244,15 @@ func TestOverviewCascadeStashInSlot(t *testing.T) { | |||
| 245 | 244 | if !within1(front.Rect.X+front.Rect.W/2, cur.X+cur.W/2) { | |
| 246 | 245 | t.Fatalf("pile not centred on the row %v: %+v", cur, front.Rect) | |
| 247 | 246 | } | |
| 248 | - | // The lane is centred in the slot; its tiles stop a gap short of its | |
| 249 | - | // bottom edge. | |
| 250 | - | rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom(horizontalAxis))) | |
| 251 | - | bottom := cur.Y + (cur.H-rowH)/2 + rowH | |
| 252 | - | if !within1(centreY(front.Rect), bottom) || front.Rect.Y >= lane.Y+lane.H { | |
| 253 | - | t.Fatalf("pile not across the lane's bottom edge %d (lane %v): %+v", bottom, lane, front.Rect) | |
| 247 | + | // The pile sits under the lane, every card of it: the lane and the | |
| 248 | + | // pile are centred together in the slot. | |
| 249 | + | for _, id := range []WindowID{40, 41} { | |
| 250 | + | if c := previewOf(t, ps, id).Rect; c.Y <= lane.Y+lane.H { | |
| 251 | + | t.Fatalf("card %d %v not under the lane %v", id, c, lane) | |
| 252 | + | } | |
| 253 | + | } | |
| 254 | + | if top, bottom := lane.Y-cur.Y, cur.Y+cur.H-(front.Rect.Y+front.Rect.H); !within1(top, bottom) && abs(top-bottom) > lane.H/10 { | |
| 255 | + | t.Fatalf("lane and pile not centred together: %d above, %d below", top, bottom) | |
| 254 | 256 | } | |
| 255 | 257 | // The pile does not shift the band: its row stays centred on the area. | |
| 256 | 258 | u := m.Current().overviewArea() | |
| @@ -341,8 +343,8 @@ func TestOverviewCascadeKeys(t *testing.T) { | |||
| 341 | 343 | ||
| 342 | 344 | // With a stash: left from the first column still goes to the | |
| 343 | 345 | // previous workspace. Down past the last band enters the | |
| 344 | - | // stash, right comes back to the band's first column, and | |
| 345 | - | // left from the stash goes to the previous workspace. | |
| 346 | + | // stash, up comes back to the band, and left past the first | |
| 347 | + | // card goes to the previous workspace. | |
| 346 | 348 | withStash(m, 2) | |
| 347 | 349 | w.FocusID(7) | |
| 348 | 350 | m.ToggleOverview() | |
| @@ -360,14 +362,16 @@ func TestOverviewCascadeKeys(t *testing.T) { | |||
| 360 | 362 | if m.cardAt(w) < 0 { | |
| 361 | 363 | t.Fatal("down past the last band did not select the stash") | |
| 362 | 364 | } | |
| 363 | - | d.move(m, 1, 0) | |
| 365 | + | d.move(m, 0, -1) | |
| 364 | 366 | if m.cardAt(w) >= 0 || sel() != 10 { | |
| 365 | - | t.Fatalf("right from the stash: card %d, column %d", m.cardAt(w), sel()) | |
| 367 | + | t.Fatalf("up from the stash: card %d, column %d", m.cardAt(w), sel()) | |
| 366 | 368 | } | |
| 367 | 369 | d.move(m, 0, 1) | |
| 368 | - | d.move(m, -1, 0) | |
| 370 | + | for range len(w.Stash) { | |
| 371 | + | d.move(m, -1, 0) | |
| 372 | + | } | |
| 369 | 373 | if m.Current() != prev { | |
| 370 | - | t.Fatal("left from the stash did not reach the previous workspace") | |
| 374 | + | t.Fatal("left past the first card did not reach the previous workspace") | |
| 371 | 375 | } | |
| 372 | 376 | m.CancelOverview() | |
| 373 | 377 | if m.Current() != w || focusedID(m) != 7 { | |
| @@ -533,22 +537,22 @@ func TestOverviewCascadeThreeFingerDownEntersStash(t *testing.T) { | |||
| 533 | 537 | } | |
| 534 | 538 | } | |
| 535 | 539 | ||
| 536 | - | // Right from a stash card lands on the maximized column, which keeps its | |
| 537 | - | // maximization: it is alone in its band. | |
| 540 | + | // Up from a stash card lands on the maximized column of the last band, | |
| 541 | + | // which keeps its maximization: it is alone in its band. | |
| 538 | 542 | func TestOverviewCascadeStashToMaximized(t *testing.T) { | |
| 539 | 543 | m := cascadeOverview() | |
| 540 | 544 | withStash(m, 1) | |
| 541 | 545 | w := m.Current() | |
| 542 | - | w.FocusID(9) | |
| 546 | + | w.FocusID(10) | |
| 543 | 547 | w.ToggleFullWidth() | |
| 544 | 548 | m.ToggleOverview() | |
| 545 | 549 | m.selectCard(w, w.stashAt) // As a click on the pile does. | |
| 546 | 550 | if m.cardAt(w) < 0 { | |
| 547 | 551 | t.Fatalf("setup: no stash card, target %d", m.overviewTarget()) | |
| 548 | 552 | } | |
| 549 | - | m.OverviewMove(1, 0) | |
| 550 | - | if m.overviewTarget() != 9 || !w.Columns[w.Focus].FullWidth || len(w.maximized) == 0 || w.maximized[0] != 9 { | |
| 551 | - | t.Fatalf("target %d, maximized %v; want 9", m.overviewTarget(), w.maximized) | |
| 553 | + | m.OverviewMove(0, -1) | |
| 554 | + | if m.overviewTarget() != 10 || !w.Columns[w.Focus].FullWidth || len(w.maximized) == 0 || w.maximized[0] != 10 { | |
| 555 | + | t.Fatalf("target %d, maximized %v; want 10", m.overviewTarget(), w.maximized) | |
| 552 | 556 | } | |
| 553 | 557 | } | |
| 554 | 558 | ||
| @@ -571,22 +575,19 @@ func TestOverviewCascadeNamedDivider(t *testing.T) { | |||
| 571 | 575 | } | |
| 572 | 576 | } | |
| 573 | 577 | ||
| 574 | - | // A card behind the front one of the pile is clicked where it shows, | |
| 575 | - | // over the selected lane's tiles. | |
| 578 | + | // A card behind the front one of the folded pile is clicked where it | |
| 579 | + | // shows, above the front card. | |
| 576 | 580 | func TestOverviewCascadeStashPeekClick(t *testing.T) { | |
| 577 | 581 | m := cascadeOverview() | |
| 578 | 582 | withStash(m, 2) | |
| 579 | 583 | m.ToggleOverview() | |
| 580 | 584 | ps := m.Layout() | |
| 581 | 585 | front, back := previewOf(t, ps, 41).Rect, previewOf(t, ps, 40).Rect | |
| 582 | - | // The strip of 40 above the front card, inside the lane's tiles. | |
| 586 | + | // The strip of 40 above the front card. | |
| 583 | 587 | x, y := float64(back.X+back.W-1), float64(back.Y) | |
| 584 | - | if back.Y >= front.Y || back.Y < previewOf(t, ps, 8).Rect.Y { | |
| 588 | + | if back.Y >= front.Y { | |
| 585 | 589 | t.Fatalf("setup: back %v front %v", back, front) | |
| 586 | 590 | } | |
| 587 | - | if id := overviewIn(ps, x, y); id == 40 { | |
| 588 | - | t.Fatal("setup: the tiles do not cover the strip") | |
| 589 | - | } | |
| 590 | 591 | if id := m.overviewHit(ps, x, y); id != 40 { | |
| 591 | 592 | t.Fatalf("clicked %d, want 40", id) | |
| 592 | 593 | } | |
| @@ -645,8 +646,8 @@ func TestOverviewCascadeNeighborStashKeepsSlot(t *testing.T) { | |||
| 645 | 646 | } | |
| 646 | 647 | } | |
| 647 | 648 | ||
| 648 | - | // Down from a covering float enters the stash; right brings the columns | |
| 649 | - | // back to the front. | |
| 649 | + | // Down from a covering float enters the stash; up comes back to the | |
| 650 | + | // float. | |
| 650 | 651 | func TestOverviewCascadeFloatStashRight(t *testing.T) { | |
| 651 | 652 | m := cascadeOverview() | |
| 652 | 653 | withStash(m, 1) | |
| @@ -661,9 +662,9 @@ func TestOverviewCascadeFloatStashRight(t *testing.T) { | |||
| 661 | 662 | if m.cardAt(w) < 0 { | |
| 662 | 663 | t.Fatal("down from the float did not select the stash") | |
| 663 | 664 | } | |
| 664 | - | m.OverviewMove(1, 0) | |
| 665 | - | if m.cardAt(w) >= 0 || m.stackFront(w).kind != stackColumns || m.overviewTarget() == 0 { | |
| 666 | - | t.Fatalf("right from the stash: card %d, front %+v, target %d", m.cardAt(w), m.stackFront(w), m.overviewTarget()) | |
| 665 | + | m.OverviewMove(0, -1) | |
| 666 | + | if m.cardAt(w) >= 0 || m.stackFront(w).kind != stackFloat || m.overviewTarget() != 90 { | |
| 667 | + | t.Fatalf("up from the stash: card %d, front %+v, target %d", m.cardAt(w), m.stackFront(w), m.overviewTarget()) | |
| 667 | 668 | } | |
| 668 | 669 | } | |
| 669 | 670 | ||
| @@ -777,19 +778,18 @@ func TestOverviewCascadeFocusBinds(t *testing.T) { | |||
| 777 | 778 | } | |
| 778 | 779 | m.CancelOverview() | |
| 779 | 780 | ||
| 780 | - | // A selected stash card keeps the pile cycle. | |
| 781 | + | // Over the unfolded stash they still change workspace: left and | |
| 782 | + | // right walk the cards. | |
| 781 | 783 | withStash(m, 3) | |
| 782 | 784 | w.FocusID(10) | |
| 783 | 785 | m.ToggleOverview() | |
| 784 | 786 | m.OverviewMove(0, 1) | |
| 785 | - | at := m.cardAt(w) | |
| 786 | - | m.overviewFocus(ActionFocusWorkspacePrev) | |
| 787 | - | if m.Current() != w || m.cardAt(w) != at-1 { | |
| 788 | - | t.Fatalf("focus-workspace-prev over a card: card %d, was %d", m.cardAt(w), at) | |
| 787 | + | if m.cardAt(w) < 0 { | |
| 788 | + | t.Fatal("setup: no card selected") | |
| 789 | 789 | } | |
| 790 | - | m.overviewFocus(ActionFocusWorkspaceNext) | |
| 791 | - | if m.Current() != w || m.cardAt(w) != at { | |
| 792 | - | t.Fatalf("focus-workspace-next over a card: card %d", m.cardAt(w)) | |
| 790 | + | m.overviewFocus(ActionFocusWorkspacePrev) | |
| 791 | + | if m.Current() != m.Workspaces[0] { | |
| 792 | + | t.Fatal("focus-workspace-prev over a card did not change workspace") | |
| 793 | 793 | } | |
| 794 | 794 | } | |
| 795 | 795 | ||
| @@ -1089,7 +1089,12 @@ func TestOverviewCascadePileVisible(t *testing.T) { | |||
| 1089 | 1089 | } | |
| 1090 | 1090 | } | |
| 1091 | 1091 | } | |
| 1092 | - | if want := min(n, overviewCards); shown != want { | |
| 1092 | + | want := min(n, overviewCards) | |
| 1093 | + | if m.cardAt(w) >= 0 { | |
| 1094 | + | // Unfolded: every card. | |
| 1095 | + | want = n | |
| 1096 | + | } | |
| 1097 | + | if shown != want { | |
| 1093 | 1098 | t.Fatalf("n=%d hidden=%v down=%d: %d cards shown, want %d", n, hidden, down, shown, want) | |
| 1094 | 1099 | } | |
| 1095 | 1100 | } | |
| @@ -2,7 +2,6 @@ package core | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | - | "math" | |||
| 6 | "slices" | 5 | "slices" | ||
| 7 | "testing" | 6 | "testing" | ||
| 8 | 7 | ||||
| @@ -245,12 +244,15 @@ func TestOverviewCascadeStashInSlot(t *testing.T) { | |||||
| 245 | if !within1(front.Rect.X+front.Rect.W/2, cur.X+cur.W/2) { | 244 | if !within1(front.Rect.X+front.Rect.W/2, cur.X+cur.W/2) { | ||
| 246 | t.Fatalf("pile not centred on the row %v: %+v", cur, front.Rect) | 245 | t.Fatalf("pile not centred on the row %v: %+v", cur, front.Rect) | ||
| 247 | } | 246 | } | ||
| 248 | - | // The lane is centred in the slot; its tiles stop a gap short of its | 247 | + | // The pile sits under the lane, every card of it: the lane and the |
| 249 | - | // bottom edge. | 248 | + | // pile are centred together in the slot. |
| 250 | - | rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom(horizontalAxis))) | 249 | + | for _, id := range []WindowID{40, 41} { |
| 251 | - | bottom := cur.Y + (cur.H-rowH)/2 + rowH | 250 | + | if c := previewOf(t, ps, id).Rect; c.Y <= lane.Y+lane.H { |
| 252 | - | if !within1(centreY(front.Rect), bottom) || front.Rect.Y >= lane.Y+lane.H { | 251 | + | t.Fatalf("card %d %v not under the lane %v", id, c, lane) |
| 253 | - | t.Fatalf("pile not across the lane's bottom edge %d (lane %v): %+v", bottom, lane, front.Rect) | 252 | + | } |
| 253 | + | } | |||
| 254 | + | if top, bottom := lane.Y-cur.Y, cur.Y+cur.H-(front.Rect.Y+front.Rect.H); !within1(top, bottom) && abs(top-bottom) > lane.H/10 { | |||
| 255 | + | t.Fatalf("lane and pile not centred together: %d above, %d below", top, bottom) | |||
| 254 | } | 256 | } | ||
| 255 | // The pile does not shift the band: its row stays centred on the area. | 257 | // The pile does not shift the band: its row stays centred on the area. | ||
| 256 | u := m.Current().overviewArea() | 258 | u := m.Current().overviewArea() | ||
| @@ -341,8 +343,8 @@ func TestOverviewCascadeKeys(t *testing.T) { | |||||
| 341 | 343 | ||||
| 342 | // 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 | ||
| 343 | // previous workspace. Down past the last band enters the | 345 | // previous workspace. Down past the last band enters the | ||
| 344 | - | // stash, right comes back to the band's first column, and | 346 | + | // stash, up comes back to the band, and left past the first |
| 345 | - | // left from the stash goes to the previous workspace. | 347 | + | // card goes to the previous workspace. |
| 346 | withStash(m, 2) | 348 | withStash(m, 2) | ||
| 347 | w.FocusID(7) | 349 | w.FocusID(7) | ||
| 348 | m.ToggleOverview() | 350 | m.ToggleOverview() | ||
| @@ -360,14 +362,16 @@ func TestOverviewCascadeKeys(t *testing.T) { | |||||
| 360 | if m.cardAt(w) < 0 { | 362 | if m.cardAt(w) < 0 { | ||
| 361 | t.Fatal("down past the last band did not select the stash") | 363 | t.Fatal("down past the last band did not select the stash") | ||
| 362 | } | 364 | } | ||
| 363 | - | d.move(m, 1, 0) | 365 | + | d.move(m, 0, -1) |
| 364 | if m.cardAt(w) >= 0 || sel() != 10 { | 366 | if m.cardAt(w) >= 0 || sel() != 10 { | ||
| 365 | - | t.Fatalf("right from the stash: card %d, column %d", m.cardAt(w), sel()) | 367 | + | t.Fatalf("up from the stash: card %d, column %d", m.cardAt(w), sel()) |
| 366 | } | 368 | } | ||
| 367 | d.move(m, 0, 1) | 369 | d.move(m, 0, 1) | ||
| 368 | - | d.move(m, -1, 0) | 370 | + | for range len(w.Stash) { |
| 371 | + | d.move(m, -1, 0) | |||
| 372 | + | } | |||
| 369 | if m.Current() != prev { | 373 | if m.Current() != prev { | ||
| 370 | - | t.Fatal("left from the stash did not reach the previous workspace") | 374 | + | t.Fatal("left past the first card did not reach the previous workspace") |
| 371 | } | 375 | } | ||
| 372 | m.CancelOverview() | 376 | m.CancelOverview() | ||
| 373 | if m.Current() != w || focusedID(m) != 7 { | 377 | if m.Current() != w || focusedID(m) != 7 { | ||
| @@ -533,22 +537,22 @@ func TestOverviewCascadeThreeFingerDownEntersStash(t *testing.T) { | |||||
| 533 | } | 537 | } | ||
| 534 | } | 538 | } | ||
| 535 | 539 | ||||
| 536 | - | // Right from a stash card lands on the maximized column, which keeps its | 540 | + | // Up from a stash card lands on the maximized column of the last band, |
| 537 | - | // maximization: it is alone in its band. | 541 | + | // which keeps its maximization: it is alone in its band. |
| 538 | func TestOverviewCascadeStashToMaximized(t *testing.T) { | 542 | func TestOverviewCascadeStashToMaximized(t *testing.T) { | ||
| 539 | m := cascadeOverview() | 543 | m := cascadeOverview() | ||
| 540 | withStash(m, 1) | 544 | withStash(m, 1) | ||
| 541 | w := m.Current() | 545 | w := m.Current() | ||
| 542 | - | w.FocusID(9) | 546 | + | w.FocusID(10) |
| 543 | w.ToggleFullWidth() | 547 | w.ToggleFullWidth() | ||
| 544 | m.ToggleOverview() | 548 | m.ToggleOverview() | ||
| 545 | m.selectCard(w, w.stashAt) // As a click on the pile does. | 549 | m.selectCard(w, w.stashAt) // As a click on the pile does. | ||
| 546 | if m.cardAt(w) < 0 { | 550 | if m.cardAt(w) < 0 { | ||
| 547 | t.Fatalf("setup: no stash card, target %d", m.overviewTarget()) | 551 | t.Fatalf("setup: no stash card, target %d", m.overviewTarget()) | ||
| 548 | } | 552 | } | ||
| 549 | - | m.OverviewMove(1, 0) | 553 | + | m.OverviewMove(0, -1) |
| 550 | - | if m.overviewTarget() != 9 || !w.Columns[w.Focus].FullWidth || len(w.maximized) == 0 || w.maximized[0] != 9 { | 554 | + | if m.overviewTarget() != 10 || !w.Columns[w.Focus].FullWidth || len(w.maximized) == 0 || w.maximized[0] != 10 { |
| 551 | - | t.Fatalf("target %d, maximized %v; want 9", m.overviewTarget(), w.maximized) | 555 | + | t.Fatalf("target %d, maximized %v; want 10", m.overviewTarget(), w.maximized) |
| 552 | } | 556 | } | ||
| 553 | } | 557 | } | ||
| 554 | 558 | ||||
| @@ -571,22 +575,19 @@ func TestOverviewCascadeNamedDivider(t *testing.T) { | |||||
| 571 | } | 575 | } | ||
| 572 | } | 576 | } | ||
| 573 | 577 | ||||
| 574 | - | // A card behind the front one of the pile is clicked where it shows, | 578 | + | // A card behind the front one of the folded pile is clicked where it |
| 575 | - | // over the selected lane's tiles. | 579 | + | // shows, above the front card. |
| 576 | func TestOverviewCascadeStashPeekClick(t *testing.T) { | 580 | func TestOverviewCascadeStashPeekClick(t *testing.T) { | ||
| 577 | m := cascadeOverview() | 581 | m := cascadeOverview() | ||
| 578 | withStash(m, 2) | 582 | withStash(m, 2) | ||
| 579 | m.ToggleOverview() | 583 | m.ToggleOverview() | ||
| 580 | ps := m.Layout() | 584 | ps := m.Layout() | ||
| 581 | front, back := previewOf(t, ps, 41).Rect, previewOf(t, ps, 40).Rect | 585 | front, back := previewOf(t, ps, 41).Rect, previewOf(t, ps, 40).Rect | ||
| 582 | - | // The strip of 40 above the front card, inside the lane's tiles. | 586 | + | // The strip of 40 above the front card. |
| 583 | x, y := float64(back.X+back.W-1), float64(back.Y) | 587 | x, y := float64(back.X+back.W-1), float64(back.Y) | ||
| 584 | - | if back.Y >= front.Y || back.Y < previewOf(t, ps, 8).Rect.Y { | 588 | + | if back.Y >= front.Y { |
| 585 | t.Fatalf("setup: back %v front %v", back, front) | 589 | t.Fatalf("setup: back %v front %v", back, front) | ||
| 586 | } | 590 | } | ||
| 587 | - | if id := overviewIn(ps, x, y); id == 40 { | |||
| 588 | - | t.Fatal("setup: the tiles do not cover the strip") | |||
| 589 | - | } | |||
| 590 | if id := m.overviewHit(ps, x, y); id != 40 { | 591 | if id := m.overviewHit(ps, x, y); id != 40 { | ||
| 591 | t.Fatalf("clicked %d, want 40", id) | 592 | t.Fatalf("clicked %d, want 40", id) | ||
| 592 | } | 593 | } | ||
| @@ -645,8 +646,8 @@ func TestOverviewCascadeNeighborStashKeepsSlot(t *testing.T) { | |||||
| 645 | } | 646 | } | ||
| 646 | } | 647 | } | ||
| 647 | 648 | ||||
| 648 | - | // Down from a covering float enters the stash; right brings the columns | 649 | + | // Down from a covering float enters the stash; up comes back to the |
| 649 | - | // back to the front. | 650 | + | // float. |
| 650 | func TestOverviewCascadeFloatStashRight(t *testing.T) { | 651 | func TestOverviewCascadeFloatStashRight(t *testing.T) { | ||
| 651 | m := cascadeOverview() | 652 | m := cascadeOverview() | ||
| 652 | withStash(m, 1) | 653 | withStash(m, 1) | ||
| @@ -661,9 +662,9 @@ func TestOverviewCascadeFloatStashRight(t *testing.T) { | |||||
| 661 | if m.cardAt(w) < 0 { | 662 | if m.cardAt(w) < 0 { | ||
| 662 | t.Fatal("down from the float did not select the stash") | 663 | t.Fatal("down from the float did not select the stash") | ||
| 663 | } | 664 | } | ||
| 664 | - | m.OverviewMove(1, 0) | 665 | + | m.OverviewMove(0, -1) |
| 665 | - | if m.cardAt(w) >= 0 || m.stackFront(w).kind != stackColumns || m.overviewTarget() == 0 { | 666 | + | if m.cardAt(w) >= 0 || m.stackFront(w).kind != stackFloat || m.overviewTarget() != 90 { |
| 666 | - | t.Fatalf("right from the stash: card %d, front %+v, target %d", m.cardAt(w), m.stackFront(w), m.overviewTarget()) | 667 | + | t.Fatalf("up from the stash: card %d, front %+v, target %d", m.cardAt(w), m.stackFront(w), m.overviewTarget()) |
| 667 | } | 668 | } | ||
| 668 | } | 669 | } | ||
| 669 | 670 | ||||
| @@ -777,19 +778,18 @@ func TestOverviewCascadeFocusBinds(t *testing.T) { | |||||
| 777 | } | 778 | } | ||
| 778 | m.CancelOverview() | 779 | m.CancelOverview() | ||
| 779 | 780 | ||||
| 780 | - | // A selected stash card keeps the pile cycle. | 781 | + | // Over the unfolded stash they still change workspace: left and |
| 782 | + | // right walk the cards. | |||
| 781 | withStash(m, 3) | 783 | withStash(m, 3) | ||
| 782 | w.FocusID(10) | 784 | w.FocusID(10) | ||
| 783 | m.ToggleOverview() | 785 | m.ToggleOverview() | ||
| 784 | m.OverviewMove(0, 1) | 786 | m.OverviewMove(0, 1) | ||
| 785 | - | at := m.cardAt(w) | 787 | + | if m.cardAt(w) < 0 { |
| 786 | - | m.overviewFocus(ActionFocusWorkspacePrev) | 788 | + | t.Fatal("setup: no card selected") |
| 787 | - | if m.Current() != w || m.cardAt(w) != at-1 { | |||
| 788 | - | t.Fatalf("focus-workspace-prev over a card: card %d, was %d", m.cardAt(w), at) | |||
| 789 | } | 789 | } | ||
| 790 | - | m.overviewFocus(ActionFocusWorkspaceNext) | 790 | + | m.overviewFocus(ActionFocusWorkspacePrev) |
| 791 | - | if m.Current() != w || m.cardAt(w) != at { | 791 | + | if m.Current() != m.Workspaces[0] { |
| 792 | - | t.Fatalf("focus-workspace-next over a card: card %d", m.cardAt(w)) | 792 | + | t.Fatal("focus-workspace-prev over a card did not change workspace") |
| 793 | } | 793 | } | ||
| 794 | } | 794 | } | ||
| 795 | 795 | ||||
| @@ -1089,7 +1089,12 @@ func TestOverviewCascadePileVisible(t *testing.T) { | |||||
| 1089 | } | 1089 | } | ||
| 1090 | } | 1090 | } | ||
| 1091 | } | 1091 | } | ||
| 1092 | - | if want := min(n, overviewCards); shown != want { | 1092 | + | want := min(n, overviewCards) |
| 1093 | + | if m.cardAt(w) >= 0 { | |||
| 1094 | + | // Unfolded: every card. | |||
| 1095 | + | want = n | |||
| 1096 | + | } | |||
| 1097 | + | if shown != want { | |||
| 1093 | t.Fatalf("n=%d hidden=%v down=%d: %d cards shown, want %d", n, hidden, down, shown, want) | 1098 | t.Fatalf("n=%d hidden=%v down=%d: %d cards shown, want %d", n, hidden, down, shown, want) | ||
| 1094 | } | 1099 | } | ||
| 1095 | } | 1100 | } | ||
A internal/core/overview_unfold_test.go +203 -0
| @@ -0,0 +1,203 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import "testing" | |
| 4 | + | ||
| 5 | + | // cards are the shown stash placements of w in ps, in stash order. | |
| 6 | + | func shownCards(t *testing.T, w *Workspace, ps []Placement) []Placement { | |
| 7 | + | t.Helper() | |
| 8 | + | var out []Placement | |
| 9 | + | for _, e := range w.Stash { | |
| 10 | + | if p := previewOf(t, ps, e.ID); !p.Hidden { | |
| 11 | + | out = append(out, p) | |
| 12 | + | } | |
| 13 | + | } | |
| 14 | + | return out | |
| 15 | + | } | |
| 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. | |
| 20 | + | func TestOverviewCascadeStashUnfolds(t *testing.T) { | |
| 21 | + | for _, d := range cascadeDrivers { | |
| 22 | + | t.Run(d.name, func(t *testing.T) { | |
| 23 | + | m := cascadeOverview() | |
| 24 | + | withStash(m, 3) | |
| 25 | + | w := m.Current() | |
| 26 | + | w.FocusID(10) | |
| 27 | + | m.ToggleOverview() | |
| 28 | + | d.move(m, 0, 1) | |
| 29 | + | at := m.cardAt(w) | |
| 30 | + | if at < 0 { | |
| 31 | + | t.Fatal("setup: no card selected") | |
| 32 | + | } | |
| 33 | + | ps := m.Layout() | |
| 34 | + | cards := shownCards(t, w, ps) | |
| 35 | + | if len(cards) != 3 { | |
| 36 | + | t.Fatalf("%d cards shown, want 3", len(cards)) | |
| 37 | + | } | |
| 38 | + | lane := previewOf(t, ps, 10).Rect | |
| 39 | + | for i, c := range cards { | |
| 40 | + | if c.Rect.Y <= lane.Y+lane.H { | |
| 41 | + | t.Fatalf("card %d %v not under the lane %v", c.ID, c.Rect, lane) | |
| 42 | + | } | |
| 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) | |
| 45 | + | } | |
| 46 | + | if c.Focused != (i == at) { | |
| 47 | + | t.Fatalf("card %d focused %v, selection %d", i, c.Focused, at) | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | for i := range 2 { | |
| 51 | + | d.move(m, -1, 0) | |
| 52 | + | if want := max(at-i-1, 0); m.Current() != w || m.cardAt(w) != want { | |
| 53 | + | t.Fatalf("left %d: card %d, want %d", i, m.cardAt(w), want) | |
| 54 | + | } | |
| 55 | + | if at-i-1 < 0 { | |
| 56 | + | break | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | for m.cardAt(w) < len(w.Stash)-1 { | |
| 60 | + | before := m.cardAt(w) | |
| 61 | + | d.move(m, 1, 0) | |
| 62 | + | if m.cardAt(w) != before+1 { | |
| 63 | + | t.Fatalf("right: card %d, was %d", m.cardAt(w), before) | |
| 64 | + | } | |
| 65 | + | } | |
| 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 | + | }) | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | // Down past the last lane enters the stash at its own selection; up leaves | |
| 76 | + | // it, and the pile folds again. | |
| 77 | + | func TestOverviewCascadeStashFolds(t *testing.T) { | |
| 78 | + | m := cascadeOverview() | |
| 79 | + | withStash(m, 3) | |
| 80 | + | w := m.Current() | |
| 81 | + | w.FocusID(10) | |
| 82 | + | m.ToggleOverview() | |
| 83 | + | folded := shownCards(t, w, m.Layout()) | |
| 84 | + | m.OverviewMove(0, 1) | |
| 85 | + | m.OverviewMove(0, -1) | |
| 86 | + | if m.cardAt(w) >= 0 { | |
| 87 | + | t.Fatal("up did not leave the stash") | |
| 88 | + | } | |
| 89 | + | again := shownCards(t, w, m.Layout()) | |
| 90 | + | if len(again) != len(folded) { | |
| 91 | + | t.Fatalf("folded again: %d cards, was %d", len(again), len(folded)) | |
| 92 | + | } | |
| 93 | + | for i := range again { | |
| 94 | + | if again[i].Rect != folded[i].Rect || again[i].Focused { | |
| 95 | + | t.Fatalf("card %d folded at %v (focused %v), was %v", again[i].ID, again[i].Rect, again[i].Focused, folded[i].Rect) | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | } | |
| 99 | + | ||
| 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) { | |
| 105 | + | for _, axis := range []layoutAxis{horizontalAxis, verticalAxis} { | |
| 106 | + | t.Run(map[layoutAxis]string{horizontalAxis: "cascade", verticalAxis: "scroll"}[axis], func(t *testing.T) { | |
| 107 | + | m := cascadeOverview() | |
| 108 | + | w := m.Current() | |
| 109 | + | if axis == verticalAxis { | |
| 110 | + | w.Overflow = OverflowScroll | |
| 111 | + | } | |
| 112 | + | withStash(m, 12) | |
| 113 | + | m.ToggleOverview() | |
| 114 | + | if m.overviewAxis() != axis { | |
| 115 | + | t.Fatal("setup: wrong overview axis") | |
| 116 | + | } | |
| 117 | + | m.selectCard(w, 0) | |
| 118 | + | u := w.overviewArea() | |
| 119 | + | pos := func(r Rect) (int, int, int, int) { | |
| 120 | + | if axis == verticalAxis { | |
| 121 | + | return r.Y, r.H, u.Y, u.Y + u.H | |
| 122 | + | } | |
| 123 | + | return r.X, r.W, u.X, u.X + u.W | |
| 124 | + | } | |
| 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 | + | } | |
| 130 | + | ps := m.Layout() | |
| 131 | + | 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) | |
| 145 | + | } | |
| 146 | + | for _, e := range w.Stash { | |
| 147 | + | hidden = hidden || previewOf(t, ps, e.ID).Hidden | |
| 148 | + | } | |
| 149 | + | if axis == verticalAxis { | |
| 150 | + | m.OverviewMove(0, 1) | |
| 151 | + | } else if i < len(w.Stash)-1 { | |
| 152 | + | m.OverviewMove(1, 0) | |
| 153 | + | } | |
| 154 | + | } | |
| 155 | + | if !hidden || !centred { | |
| 156 | + | t.Fatalf("12 cards: hidden %v, centred %v; the test does not scroll", hidden, centred) | |
| 157 | + | } | |
| 158 | + | }) | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | // A scroll overview unfolds its pile into a column on the left of the row; | |
| 163 | + | // up and down walk it, right goes back to the columns. | |
| 164 | + | func TestOverviewScrollStashUnfolds(t *testing.T) { | |
| 165 | + | m := overviewMonitor() | |
| 166 | + | w := m.Current() | |
| 167 | + | for id := WindowID(40); id < 43; id++ { | |
| 168 | + | w.AddWindow(id) | |
| 169 | + | w.FocusID(id) | |
| 170 | + | w.ToggleWindowStash() | |
| 171 | + | } | |
| 172 | + | w.FocusID(1) | |
| 173 | + | m.ToggleOverview() | |
| 174 | + | if m.overviewAxis() != verticalAxis { | |
| 175 | + | t.Fatal("setup: not a vertical overview") | |
| 176 | + | } | |
| 177 | + | m.OverviewMove(-1, 0) | |
| 178 | + | if m.cardAt(w) < 0 { | |
| 179 | + | t.Fatal("left from the first column did not select the stash") | |
| 180 | + | } | |
| 181 | + | cards := shownCards(t, w, m.Layout()) | |
| 182 | + | if len(cards) != 3 { | |
| 183 | + | t.Fatalf("%d cards shown, want 3", len(cards)) | |
| 184 | + | } | |
| 185 | + | for i := 1; i < len(cards); i++ { | |
| 186 | + | if cards[i].Rect.X != cards[0].Rect.X || cards[i].Rect.Y < cards[i-1].Rect.Y+cards[i-1].Rect.H { | |
| 187 | + | t.Fatalf("cards not stacked down: %v then %v", cards[i-1].Rect, cards[i].Rect) | |
| 188 | + | } | |
| 189 | + | } | |
| 190 | + | m.OverviewMove(0, -1) | |
| 191 | + | m.OverviewMove(0, -1) | |
| 192 | + | if m.cardAt(w) != 0 { | |
| 193 | + | t.Fatalf("up: card %d, want 0", m.cardAt(w)) | |
| 194 | + | } | |
| 195 | + | m.OverviewMove(0, 1) | |
| 196 | + | if m.cardAt(w) != 1 { | |
| 197 | + | t.Fatalf("down: card %d, want 1", m.cardAt(w)) | |
| 198 | + | } | |
| 199 | + | m.OverviewMove(1, 0) | |
| 200 | + | if m.cardAt(w) >= 0 || m.overviewTarget() == 0 { | |
| 201 | + | t.Fatalf("right: card %d, target %d", m.cardAt(w), m.overviewTarget()) | |
| 202 | + | } | |
| 203 | + | } | |
| @@ -0,0 +1,203 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import "testing" | |||
| 4 | + | ||||
| 5 | + | // cards are the shown stash placements of w in ps, in stash order. | |||
| 6 | + | func shownCards(t *testing.T, w *Workspace, ps []Placement) []Placement { | |||
| 7 | + | t.Helper() | |||
| 8 | + | var out []Placement | |||
| 9 | + | for _, e := range w.Stash { | |||
| 10 | + | if p := previewOf(t, ps, e.ID); !p.Hidden { | |||
| 11 | + | out = append(out, p) | |||
| 12 | + | } | |||
| 13 | + | } | |||
| 14 | + | return out | |||
| 15 | + | } | |||
| 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. | |||
| 20 | + | func TestOverviewCascadeStashUnfolds(t *testing.T) { | |||
| 21 | + | for _, d := range cascadeDrivers { | |||
| 22 | + | t.Run(d.name, func(t *testing.T) { | |||
| 23 | + | m := cascadeOverview() | |||
| 24 | + | withStash(m, 3) | |||
| 25 | + | w := m.Current() | |||
| 26 | + | w.FocusID(10) | |||
| 27 | + | m.ToggleOverview() | |||
| 28 | + | d.move(m, 0, 1) | |||
| 29 | + | at := m.cardAt(w) | |||
| 30 | + | if at < 0 { | |||
| 31 | + | t.Fatal("setup: no card selected") | |||
| 32 | + | } | |||
| 33 | + | ps := m.Layout() | |||
| 34 | + | cards := shownCards(t, w, ps) | |||
| 35 | + | if len(cards) != 3 { | |||
| 36 | + | t.Fatalf("%d cards shown, want 3", len(cards)) | |||
| 37 | + | } | |||
| 38 | + | lane := previewOf(t, ps, 10).Rect | |||
| 39 | + | for i, c := range cards { | |||
| 40 | + | if c.Rect.Y <= lane.Y+lane.H { | |||
| 41 | + | t.Fatalf("card %d %v not under the lane %v", c.ID, c.Rect, lane) | |||
| 42 | + | } | |||
| 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) | |||
| 45 | + | } | |||
| 46 | + | if c.Focused != (i == at) { | |||
| 47 | + | t.Fatalf("card %d focused %v, selection %d", i, c.Focused, at) | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | for i := range 2 { | |||
| 51 | + | d.move(m, -1, 0) | |||
| 52 | + | if want := max(at-i-1, 0); m.Current() != w || m.cardAt(w) != want { | |||
| 53 | + | t.Fatalf("left %d: card %d, want %d", i, m.cardAt(w), want) | |||
| 54 | + | } | |||
| 55 | + | if at-i-1 < 0 { | |||
| 56 | + | break | |||
| 57 | + | } | |||
| 58 | + | } | |||
| 59 | + | for m.cardAt(w) < len(w.Stash)-1 { | |||
| 60 | + | before := m.cardAt(w) | |||
| 61 | + | d.move(m, 1, 0) | |||
| 62 | + | if m.cardAt(w) != before+1 { | |||
| 63 | + | t.Fatalf("right: card %d, was %d", m.cardAt(w), before) | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 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 | + | }) | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | // Down past the last lane enters the stash at its own selection; up leaves | |||
| 76 | + | // it, and the pile folds again. | |||
| 77 | + | func TestOverviewCascadeStashFolds(t *testing.T) { | |||
| 78 | + | m := cascadeOverview() | |||
| 79 | + | withStash(m, 3) | |||
| 80 | + | w := m.Current() | |||
| 81 | + | w.FocusID(10) | |||
| 82 | + | m.ToggleOverview() | |||
| 83 | + | folded := shownCards(t, w, m.Layout()) | |||
| 84 | + | m.OverviewMove(0, 1) | |||
| 85 | + | m.OverviewMove(0, -1) | |||
| 86 | + | if m.cardAt(w) >= 0 { | |||
| 87 | + | t.Fatal("up did not leave the stash") | |||
| 88 | + | } | |||
| 89 | + | again := shownCards(t, w, m.Layout()) | |||
| 90 | + | if len(again) != len(folded) { | |||
| 91 | + | t.Fatalf("folded again: %d cards, was %d", len(again), len(folded)) | |||
| 92 | + | } | |||
| 93 | + | for i := range again { | |||
| 94 | + | if again[i].Rect != folded[i].Rect || again[i].Focused { | |||
| 95 | + | t.Fatalf("card %d folded at %v (focused %v), was %v", again[i].ID, again[i].Rect, again[i].Focused, folded[i].Rect) | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | } | |||
| 99 | + | ||||
| 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) { | |||
| 105 | + | for _, axis := range []layoutAxis{horizontalAxis, verticalAxis} { | |||
| 106 | + | t.Run(map[layoutAxis]string{horizontalAxis: "cascade", verticalAxis: "scroll"}[axis], func(t *testing.T) { | |||
| 107 | + | m := cascadeOverview() | |||
| 108 | + | w := m.Current() | |||
| 109 | + | if axis == verticalAxis { | |||
| 110 | + | w.Overflow = OverflowScroll | |||
| 111 | + | } | |||
| 112 | + | withStash(m, 12) | |||
| 113 | + | m.ToggleOverview() | |||
| 114 | + | if m.overviewAxis() != axis { | |||
| 115 | + | t.Fatal("setup: wrong overview axis") | |||
| 116 | + | } | |||
| 117 | + | m.selectCard(w, 0) | |||
| 118 | + | u := w.overviewArea() | |||
| 119 | + | pos := func(r Rect) (int, int, int, int) { | |||
| 120 | + | if axis == verticalAxis { | |||
| 121 | + | return r.Y, r.H, u.Y, u.Y + u.H | |||
| 122 | + | } | |||
| 123 | + | return r.X, r.W, u.X, u.X + u.W | |||
| 124 | + | } | |||
| 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 | + | } | |||
| 130 | + | ps := m.Layout() | |||
| 131 | + | 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) | |||
| 145 | + | } | |||
| 146 | + | for _, e := range w.Stash { | |||
| 147 | + | hidden = hidden || previewOf(t, ps, e.ID).Hidden | |||
| 148 | + | } | |||
| 149 | + | if axis == verticalAxis { | |||
| 150 | + | m.OverviewMove(0, 1) | |||
| 151 | + | } else if i < len(w.Stash)-1 { | |||
| 152 | + | m.OverviewMove(1, 0) | |||
| 153 | + | } | |||
| 154 | + | } | |||
| 155 | + | if !hidden || !centred { | |||
| 156 | + | t.Fatalf("12 cards: hidden %v, centred %v; the test does not scroll", hidden, centred) | |||
| 157 | + | } | |||
| 158 | + | }) | |||
| 159 | + | } | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | // A scroll overview unfolds its pile into a column on the left of the row; | |||
| 163 | + | // up and down walk it, right goes back to the columns. | |||
| 164 | + | func TestOverviewScrollStashUnfolds(t *testing.T) { | |||
| 165 | + | m := overviewMonitor() | |||
| 166 | + | w := m.Current() | |||
| 167 | + | for id := WindowID(40); id < 43; id++ { | |||
| 168 | + | w.AddWindow(id) | |||
| 169 | + | w.FocusID(id) | |||
| 170 | + | w.ToggleWindowStash() | |||
| 171 | + | } | |||
| 172 | + | w.FocusID(1) | |||
| 173 | + | m.ToggleOverview() | |||
| 174 | + | if m.overviewAxis() != verticalAxis { | |||
| 175 | + | t.Fatal("setup: not a vertical overview") | |||
| 176 | + | } | |||
| 177 | + | m.OverviewMove(-1, 0) | |||
| 178 | + | if m.cardAt(w) < 0 { | |||
| 179 | + | t.Fatal("left from the first column did not select the stash") | |||
| 180 | + | } | |||
| 181 | + | cards := shownCards(t, w, m.Layout()) | |||
| 182 | + | if len(cards) != 3 { | |||
| 183 | + | t.Fatalf("%d cards shown, want 3", len(cards)) | |||
| 184 | + | } | |||
| 185 | + | for i := 1; i < len(cards); i++ { | |||
| 186 | + | if cards[i].Rect.X != cards[0].Rect.X || cards[i].Rect.Y < cards[i-1].Rect.Y+cards[i-1].Rect.H { | |||
| 187 | + | t.Fatalf("cards not stacked down: %v then %v", cards[i-1].Rect, cards[i].Rect) | |||
| 188 | + | } | |||
| 189 | + | } | |||
| 190 | + | m.OverviewMove(0, -1) | |||
| 191 | + | m.OverviewMove(0, -1) | |||
| 192 | + | if m.cardAt(w) != 0 { | |||
| 193 | + | t.Fatalf("up: card %d, want 0", m.cardAt(w)) | |||
| 194 | + | } | |||
| 195 | + | m.OverviewMove(0, 1) | |||
| 196 | + | if m.cardAt(w) != 1 { | |||
| 197 | + | t.Fatalf("down: card %d, want 1", m.cardAt(w)) | |||
| 198 | + | } | |||
| 199 | + | m.OverviewMove(1, 0) | |||
| 200 | + | if m.cardAt(w) >= 0 || m.overviewTarget() == 0 { | |||
| 201 | + | t.Fatalf("right: card %d, target %d", m.cardAt(w), m.overviewTarget()) | |||
| 202 | + | } | |||
| 203 | + | } | |||