feat(core): centre the cascade overview stash across the lane's bottom edge
In a horizontal overview the stash pile no longer takes room on the left of its row: it is centred on the row, straddling the bottom edge of the selected lane, over the tiles. Down (keys or three fingers) past the last lane enters it; left from the first column goes to the previous workspace. Right from the stash brings the columns to the front.
- commit
- 594ac4d7db083e326ae4653e2b2de37b2812b24b
- author
- brice · · 2026-10-14 19:19 UTC
- parent
- f4ebc85
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +221 -128
M internal/core/layout_buffers_test.go +3 -3
| @@ -146,7 +146,7 @@ func TestLayoutResultsDoNotAlias(t *testing.T) { | |||
| 146 | 146 | buf = m.layoutInto(buf) | |
| 147 | 147 | buf = m.Current().layoutInto(buf) | |
| 148 | 148 | m.Current().previewTiles() | |
| 149 | - | m.Current().overviewZoom() | |
| 149 | + | m.Current().overviewZoom(m.overviewAxis()) | |
| 150 | 150 | } | |
| 151 | 151 | // layoutInto builds in the caller's buffer, never in a scratch of | |
| 152 | 152 | // the workspace: with the capacity there it returns that storage. | |
| @@ -164,8 +164,8 @@ func TestLayoutResultsDoNotAlias(t *testing.T) { | |||
| 164 | 164 | // unchanged. | |
| 165 | 165 | mine := w.layoutInto(make([]Placement, 0, len(res))) | |
| 166 | 166 | mineKept := slices.Clone(mine) | |
| 167 | - | w.overviewZoom() | |
| 168 | - | w.peekStep() | |
| 167 | + | w.overviewZoom(m.overviewAxis()) | |
| 168 | + | w.peekStep(m.overviewAxis()) | |
| 169 | 169 | w.previewTiles() | |
| 170 | 170 | w.columnRects() | |
| 171 | 171 | _ = w.Layout() | |
| @@ -146,7 +146,7 @@ func TestLayoutResultsDoNotAlias(t *testing.T) { | |||||
| 146 | buf = m.layoutInto(buf) | 146 | buf = m.layoutInto(buf) | ||
| 147 | buf = m.Current().layoutInto(buf) | 147 | buf = m.Current().layoutInto(buf) | ||
| 148 | m.Current().previewTiles() | 148 | m.Current().previewTiles() | ||
| 149 | - | m.Current().overviewZoom() | 149 | + | m.Current().overviewZoom(m.overviewAxis()) |
| 150 | } | 150 | } | ||
| 151 | // layoutInto builds in the caller's buffer, never in a scratch of | 151 | // layoutInto builds in the caller's buffer, never in a scratch of | ||
| 152 | // the workspace: with the capacity there it returns that storage. | 152 | // the workspace: with the capacity there it returns that storage. | ||
| @@ -164,8 +164,8 @@ func TestLayoutResultsDoNotAlias(t *testing.T) { | |||||
| 164 | // unchanged. | 164 | // unchanged. | ||
| 165 | mine := w.layoutInto(make([]Placement, 0, len(res))) | 165 | mine := w.layoutInto(make([]Placement, 0, len(res))) | ||
| 166 | mineKept := slices.Clone(mine) | 166 | mineKept := slices.Clone(mine) | ||
| 167 | - | w.overviewZoom() | 167 | + | w.overviewZoom(m.overviewAxis()) |
| 168 | - | w.peekStep() | 168 | + | w.peekStep(m.overviewAxis()) |
| 169 | w.previewTiles() | 169 | w.previewTiles() | ||
| 170 | w.columnRects() | 170 | w.columnRects() | ||
| 171 | _ = w.Layout() | 171 | _ = w.Layout() | ||
M internal/core/overview.go +88 -84
| @@ -53,10 +53,12 @@ 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 pinned to the left edge of | |
| 57 | - | // its row: the selected stashed window in front at overviewCardZoom, up to | |
| 58 | - | // overviewCards-1 others behind it, each overviewCardStep further up and | |
| 59 | - | // right, dimmed. The row takes the width the pile leaves. | |
| 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. | |
| 60 | 62 | const ( | |
| 61 | 63 | overviewCardZoom = 0.2 | |
| 62 | 64 | overviewCards = 4 | |
| @@ -423,15 +425,22 @@ func (m *Monitor) cycleCard(w *Workspace, d int) bool { | |||
| 423 | 425 | // overviewLane is the vertical move of an overview whose workspaces sit side | |
| 424 | 426 | // by side (cascade): a selected stash card cycles the pile; else the | |
| 425 | 427 | // selection goes to the previous or next lane (band), at the closest column, | |
| 426 | - | // stopping at the first and last. Covering floats stack along the horizontal | |
| 427 | - | // axis, not this one. | |
| 428 | + | // stopping at the first. Down past the last lane, or from a card that is not | |
| 429 | + | // the columns, enters the stash, which sits below. Covering floats stack | |
| 430 | + | // along the horizontal axis, not this one. | |
| 428 | 431 | func (m *Monitor) overviewLane(dy int) { | |
| 429 | 432 | w := m.Current() | |
| 430 | - | if m.cycleCard(w, dy) || m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 { | |
| 433 | + | if m.cycleCard(w, dy) { | |
| 431 | 434 | return | |
| 432 | 435 | } | |
| 433 | - | if i := w.laneNeighbor(dy); i >= 0 { | |
| 434 | - | m.selectLaneColumn(w, i) | |
| 436 | + | if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 { | |
| 437 | + | if i := w.laneNeighbor(dy); i >= 0 { | |
| 438 | + | m.selectLaneColumn(w, i) | |
| 439 | + | return | |
| 440 | + | } | |
| 441 | + | } | |
| 442 | + | if dy > 0 && len(w.Stash) > 0 { | |
| 443 | + | m.selectCard(w, w.stashAt) | |
| 435 | 444 | } | |
| 436 | 445 | } | |
| 437 | 446 | ||
| @@ -444,10 +453,11 @@ func (m *Monitor) selectLaneColumn(w *Workspace, i int) { | |||
| 444 | 453 | ||
| 445 | 454 | // overviewAcross is the horizontal move of an overview whose workspaces sit | |
| 446 | 455 | // side by side (cascade): a column inside the selected lane. Past the | |
| 447 | - | // lane's first column it goes to the stash, else to the previous workspace; | |
| 448 | - | // past the last column, to the next workspace. Covering floats fan along | |
| 449 | - | // this axis and are traversed before the stash or a workspace: the card on | |
| 450 | - | // the left is the one behind. | |
| 456 | + | // lane's first column it goes to the previous workspace; past the last | |
| 457 | + | // column, to the next workspace. From a stash card, left goes to the | |
| 458 | + | // previous workspace and right back to the columns. Covering floats fan | |
| 459 | + | // along this axis and are traversed before a workspace: the card on the | |
| 460 | + | // left is the one behind. | |
| 451 | 461 | func (m *Monitor) overviewAcross(dx int) { | |
| 452 | 462 | w := m.Current() | |
| 453 | 463 | if m.cardAt(w) >= 0 { | |
| @@ -459,15 +469,18 @@ func (m *Monitor) overviewAcross(dx int) { | |||
| 459 | 469 | } | |
| 460 | 470 | m.clearCard() | |
| 461 | 471 | m.ov.selected = 0 | |
| 462 | - | if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 { | |
| 463 | - | // The first column of the focused lane. | |
| 472 | + | if len(w.Columns) > 0 { | |
| 473 | + | // The columns come to the front, even when down entered the | |
| 474 | + | // stash from a covering float: at the first column of the | |
| 475 | + | // focused lane. | |
| 476 | + | m.setFront(w, stackItem{kind: stackColumns}) | |
| 464 | 477 | i, _ := w.laneBounds(w.laneOf(w.Focus)) | |
| 465 | 478 | m.selectLaneColumn(w, i) | |
| 466 | 479 | } | |
| 467 | 480 | return | |
| 468 | 481 | } | |
| 469 | 482 | if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 { | |
| 470 | - | m.laneEdge(w, dx) | |
| 483 | + | m.laneEdge(dx) | |
| 471 | 484 | return | |
| 472 | 485 | } | |
| 473 | 486 | first, last := w.laneBounds(w.laneOf(w.Focus)) | |
| @@ -475,20 +488,15 @@ func (m *Monitor) overviewAcross(dx int) { | |||
| 475 | 488 | m.selectLaneColumn(w, next) | |
| 476 | 489 | return | |
| 477 | 490 | } | |
| 478 | - | m.laneEdge(w, dx) | |
| 491 | + | m.laneEdge(dx) | |
| 479 | 492 | } | |
| 480 | 493 | ||
| 481 | 494 | // laneEdge is a horizontal move off the edge of the selected lane, or off a | |
| 482 | - | // card that is not the columns: the stack first, then the stash on the left | |
| 483 | - | // or a workspace. | |
| 484 | - | func (m *Monitor) laneEdge(w *Workspace, dx int) { | |
| 495 | + | // card that is not the columns: the stack first, then a workspace. | |
| 496 | + | func (m *Monitor) laneEdge(dx int) { | |
| 485 | 497 | if m.moveStack(-dx) { | |
| 486 | 498 | return | |
| 487 | 499 | } | |
| 488 | - | if dx < 0 && len(w.Stash) > 0 { | |
| 489 | - | m.selectCard(w, w.stashAt) | |
| 490 | - | return | |
| 491 | - | } | |
| 492 | 500 | m.overviewStep(dx) | |
| 493 | 501 | } | |
| 494 | 502 | ||
| @@ -628,8 +636,8 @@ func (m *Monitor) overviewAxis() layoutAxis { return m.Current().policy().worksp | |||
| 628 | 636 | // | |
| 629 | 637 | // Vertical axis: full-width slots, the current one centred; the rule is | |
| 630 | 638 | // horizontal. Horizontal axis: full-height slots as wide as their content, | |
| 631 | - | // stash pile and stack peeks included, the current one centred; the rule is | |
| 632 | - | // vertical, in the spacing between two slots. | |
| 639 | + | // stack peeks included (the stash pile sits over the row), the current one | |
| 640 | + | // centred; the rule is vertical, in the spacing between two slots. | |
| 633 | 641 | func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | |
| 634 | 642 | cur := m.Current() | |
| 635 | 643 | u := cur.overviewArea() | |
| @@ -637,13 +645,10 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | |||
| 637 | 645 | axis := m.overviewAxis() | |
| 638 | 646 | var cs Rect | |
| 639 | 647 | if axis == horizontalAxis { | |
| 640 | - | // The row stays centred on the area; the pile extends the slot to | |
| 641 | - | // the left, pushing the previous workspace, unless it would leave | |
| 642 | - | // the area. | |
| 643 | - | pile, row := cur.pileWidth(), cur.rowWidth() | |
| 644 | - | cs = Rect{X: max(u.X+(u.W-row)/2-pile, u.X), Y: u.Y, W: pile + row, H: u.H} | |
| 648 | + | row := cur.rowWidth(axis) | |
| 649 | + | cs = Rect{X: u.X + (u.W-row)/2, Y: u.Y, W: row, H: u.H} | |
| 645 | 650 | } else { | |
| 646 | - | hc := cur.rowHeight() | |
| 651 | + | hc := cur.rowHeight(axis) | |
| 647 | 652 | cs = Rect{X: u.X, Y: u.Y + (u.H-hc)/2, W: u.W, H: hc} | |
| 648 | 653 | } | |
| 649 | 654 | rows := map[*Workspace]Rect{cur: cs} | |
| @@ -673,7 +678,7 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | |||
| 673 | 678 | // slot cs along axis, spacing away; gapAt is where the spacing begins. | |
| 674 | 679 | func neighborSlot(axis layoutAxis, u, cs Rect, w *Workspace, before bool, spacing int) (slot Rect, gapAt int) { | |
| 675 | 680 | if axis == horizontalAxis { | |
| 676 | - | slot = Rect{Y: u.Y, W: w.slotWidth(), H: u.H} | |
| 681 | + | slot = Rect{Y: u.Y, W: w.rowWidth(axis), H: u.H} | |
| 677 | 682 | if before { | |
| 678 | 683 | gapAt = cs.X - spacing | |
| 679 | 684 | slot.X = gapAt - slot.W | |
| @@ -683,7 +688,7 @@ func neighborSlot(axis layoutAxis, u, cs Rect, w *Workspace, before bool, spacin | |||
| 683 | 688 | } | |
| 684 | 689 | return slot, gapAt | |
| 685 | 690 | } | |
| 686 | - | h := w.rowHeight() | |
| 691 | + | h := w.rowHeight(axis) | |
| 687 | 692 | slot = Rect{X: u.X, W: u.W, H: h} | |
| 688 | 693 | if before { | |
| 689 | 694 | gapAt = cs.Y - spacing | |
| @@ -718,7 +723,7 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement { | |||
| 718 | 723 | at := m.cardAt(w) | |
| 719 | 724 | result = append(result, m.stackRow(w, axis, slot, w != cur, at < 0)...) | |
| 720 | 725 | if len(w.Stash) > 0 { | |
| 721 | - | result = append(result, w.pile(w.pileBand(axis, slot), w != cur, at)...) | |
| 726 | + | result = append(result, w.pile(axis, slot, w != cur, at)...) | |
| 722 | 727 | } | |
| 723 | 728 | } | |
| 724 | 729 | for _, f := range w.Floats { | |
| @@ -802,8 +807,8 @@ func fan(cards [][]Placement, front, maxBehind, maxBefore int, circular bool, | |||
| 802 | 807 | return out | |
| 803 | 808 | } | |
| 804 | 809 | ||
| 805 | - | // pileWidth is the room the stash pile of w takes on the left of its row, | |
| 806 | - | // its margin included; 0 without a stash. | |
| 810 | + | // pileWidth is the room the stash pile of w takes on the left of its row in | |
| 811 | + | // a vertical overview, its margin included; 0 without a stash. | |
| 807 | 812 | func (w *Workspace) pileWidth() int { | |
| 808 | 813 | if len(w.Stash) == 0 { | |
| 809 | 814 | return 0 | |
| @@ -820,11 +825,11 @@ func (w *Workspace) cardStep() int { | |||
| 820 | 825 | return min(overviewCardStep, a.W/100, a.H/50) | |
| 821 | 826 | } | |
| 822 | 827 | ||
| 823 | - | // pile places the stash of w as cards on the left of band (see pileBand): | |
| 824 | - | // entry front (the stash selection when -1) in | |
| 828 | + | // pile places the stash of w as cards in its slot along axis (see the | |
| 829 | + | // overview's stash comment): entry front (the stash selection when -1) in | |
| 825 | 830 | // front, the next ones behind it, dimmed, the rest hidden. Only a front card | |
| 826 | 831 | // of the current row (dim false) with front set is selected. | |
| 827 | - | func (w *Workspace) pile(band Rect, dim bool, front int) []Placement { | |
| 832 | + | func (w *Workspace) pile(axis layoutAxis, slot Rect, dim bool, front int) []Placement { | |
| 828 | 833 | n := len(w.Stash) | |
| 829 | 834 | if n == 0 { | |
| 830 | 835 | return nil | |
| @@ -838,10 +843,19 @@ func (w *Workspace) pile(band Rect, dim bool, front int) []Placement { | |||
| 838 | 843 | u := w.overviewArea() | |
| 839 | 844 | cw := int(math.Round(float64(r.W) * overviewCardZoom)) | |
| 840 | 845 | ch := int(math.Round(float64(r.H) * overviewCardZoom)) | |
| 841 | - | rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom())) | |
| 842 | - | // Stack rows reserve two peek steps above the front card. Align the | |
| 843 | - | // stash with that front band, not with the top of its envelope. | |
| 844 | - | x0, y0 := band.X+u.W/100, band.Y+(rowH-ch)/2 | |
| 846 | + | rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom(axis))) | |
| 847 | + | var x0, y0 int | |
| 848 | + | if axis == horizontalAxis { | |
| 849 | + | // Centred on the row, across the bottom edge of the selected | |
| 850 | + | // lane, which is centred in the slot. | |
| 851 | + | x0 = slot.X + (slot.W-cw)/2 | |
| 852 | + | y0 = slot.Y + (slot.H-rowH)/2 + rowH - ch/2 | |
| 853 | + | } else { | |
| 854 | + | // Stack rows reserve two peek steps above the front card. Align | |
| 855 | + | // the stash with that front band, not with the top of its envelope. | |
| 856 | + | band := w.frontSlot(axis, slot) | |
| 857 | + | x0, y0 = band.X+u.W/100, band.Y+(rowH-ch)/2 | |
| 858 | + | } | |
| 845 | 859 | cards := make([][]Placement, n) | |
| 846 | 860 | for i := range w.Stash { | |
| 847 | 861 | cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: Rect{X: x0, Y: y0, W: cw, H: ch}, Preview: overviewCardZoom, Focused: selected}} | |
| @@ -853,22 +867,26 @@ func (w *Workspace) pile(band Rect, dim bool, front int) []Placement { | |||
| 853 | 867 | } | |
| 854 | 868 | ||
| 855 | 869 | // overviewZoom is the scale that fits every column of w in the usable | |
| 856 | - | // width, within the overview bounds. | |
| 857 | - | func (w *Workspace) overviewZoom() float64 { | |
| 858 | - | z, _ := w.zoomSpan() | |
| 870 | + | // width, within the overview bounds, in an overview along axis: only a | |
| 871 | + | // vertical one sets the stash pile beside the row. | |
| 872 | + | func (w *Workspace) overviewZoom(axis layoutAxis) float64 { | |
| 873 | + | z, _ := w.zoomSpan(axis) | |
| 859 | 874 | return z | |
| 860 | 875 | } | |
| 861 | 876 | ||
| 862 | 877 | // zoomSpan is overviewZoom with the unscaled span of the tiles it fits. A | |
| 863 | 878 | // cascade workspace spans one screen whatever its band count. | |
| 864 | - | func (w *Workspace) zoomSpan() (float64, int) { | |
| 879 | + | func (w *Workspace) zoomSpan(axis layoutAxis) (float64, int) { | |
| 865 | 880 | // Only the span is read: the tiles go in the zoom scratch, dead on return. | |
| 866 | 881 | var span int | |
| 867 | 882 | w.zoomBuf, span, _ = w.previewTilesInto(w.zoomBuf[:0]) | |
| 868 | 883 | if span <= 0 { | |
| 869 | 884 | return overviewMaxZoom, span | |
| 870 | 885 | } | |
| 871 | - | room := float64(w.overviewArea().W-w.pileWidth()) * 0.96 | |
| 886 | + | room := float64(w.overviewArea().W) * 0.96 | |
| 887 | + | if axis == verticalAxis { | |
| 888 | + | room = float64(w.overviewArea().W-w.pileWidth()) * 0.96 | |
| 889 | + | } | |
| 872 | 890 | z := min(max(room/float64(span), overviewMinZoom), overviewMaxZoom) | |
| 873 | 891 | if w.stacked() { | |
| 874 | 892 | // Reserve two clear peeks on either side of the front row. | |
| @@ -877,31 +895,25 @@ func (w *Workspace) zoomSpan() (float64, int) { | |||
| 877 | 895 | return z, span | |
| 878 | 896 | } | |
| 879 | 897 | ||
| 880 | - | // slotWidth is the width of the slot of w in a horizontal overview: its | |
| 881 | - | // stash pile and its row. | |
| 882 | - | func (w *Workspace) slotWidth() int { | |
| 883 | - | return w.pileWidth() + w.rowWidth() | |
| 884 | - | } | |
| 885 | - | ||
| 886 | - | // rowWidth is the width of the row of w in a horizontal overview: what its | |
| 887 | - | // tiles take (at most what the pile leaves of the area), plus the room for | |
| 888 | - | // the covering floats peeking on either side of the front card. | |
| 889 | - | func (w *Workspace) rowWidth() int { | |
| 890 | - | z, span := w.zoomSpan() | |
| 898 | + | // rowWidth is the width of the slot of w in a horizontal overview: what its | |
| 899 | + | // tiles take (at most the area), plus the room for the covering floats | |
| 900 | + | // peeking on either side of the front card. | |
| 901 | + | func (w *Workspace) rowWidth(axis layoutAxis) int { | |
| 902 | + | z, span := w.zoomSpan(axis) | |
| 891 | 903 | if span <= 0 { | |
| 892 | 904 | // An empty workspace keeps the room of a screen. | |
| 893 | 905 | span = w.Usable.W | |
| 894 | 906 | } | |
| 895 | - | width := min(int(math.Round(float64(span)*z)), w.overviewArea().W-w.pileWidth()) | |
| 907 | + | width := min(int(math.Round(float64(span)*z)), w.overviewArea().W) | |
| 896 | 908 | if w.stacked() { | |
| 897 | - | width += 4 * w.peekStep() | |
| 909 | + | width += 4 * w.peekStep(axis) | |
| 898 | 910 | } | |
| 899 | 911 | return width | |
| 900 | 912 | } | |
| 901 | 913 | ||
| 902 | 914 | // peekStep is the shared step for a stack row and its front-card inset. | |
| 903 | - | func (w *Workspace) peekStep() int { | |
| 904 | - | return max(1, int(math.Round(float64(w.Usable.H)*w.overviewZoom()*overviewPeekStep))) | |
| 915 | + | func (w *Workspace) peekStep(axis layoutAxis) int { | |
| 916 | + | return max(1, int(math.Round(float64(w.Usable.H)*w.overviewZoom(axis)*overviewPeekStep))) | |
| 905 | 917 | } | |
| 906 | 918 | ||
| 907 | 919 | // frontSlot is the part of slot the front card of w takes: slot shifted | |
| @@ -910,7 +922,7 @@ func (w *Workspace) peekStep() int { | |||
| 910 | 922 | // the room of the two after it. A single-card row keeps slot. | |
| 911 | 923 | func (w *Workspace) frontSlot(axis layoutAxis, slot Rect) Rect { | |
| 912 | 924 | if w.stacked() { | |
| 913 | - | p := w.peekStep() | |
| 925 | + | p := w.peekStep(axis) | |
| 914 | 926 | axis.offset(&slot, 2*p) | |
| 915 | 927 | if axis == horizontalAxis { | |
| 916 | 928 | slot.W -= 4 * p | |
| @@ -919,24 +931,12 @@ func (w *Workspace) frontSlot(axis layoutAxis, slot Rect) Rect { | |||
| 919 | 931 | return slot | |
| 920 | 932 | } | |
| 921 | 933 | ||
| 922 | - | // pileBand is the band the stash pile of w aligns with in its slot. A | |
| 923 | - | // vertical overview aligns it with the front card (see frontSlot). A | |
| 924 | - | // horizontal one puts it level with the selected lane, left of the whole | |
| 925 | - | // stack of cards. | |
| 926 | - | func (w *Workspace) pileBand(axis layoutAxis, slot Rect) Rect { | |
| 927 | - | if axis == verticalAxis { | |
| 928 | - | return w.frontSlot(axis, slot) | |
| 929 | - | } | |
| 930 | - | slot.Y += (slot.H - int(math.Round(float64(w.Usable.H)*w.overviewZoom()))) / 2 | |
| 931 | - | return slot | |
| 932 | - | } | |
| 933 | - | ||
| 934 | 934 | // rowHeight includes the largest possible stack envelope (two peeks above | |
| 935 | 935 | // and two below). Neighbor rows use the same measure as the current row. | |
| 936 | - | func (w *Workspace) rowHeight() int { | |
| 937 | - | h := int(math.Round(float64(w.Usable.H) * w.overviewZoom())) | |
| 936 | + | func (w *Workspace) rowHeight(axis layoutAxis) int { | |
| 937 | + | h := int(math.Round(float64(w.Usable.H) * w.overviewZoom(axis))) | |
| 938 | 938 | if w.stacked() { | |
| 939 | - | h += 4 * w.peekStep() | |
| 939 | + | h += 4 * w.peekStep(axis) | |
| 940 | 940 | } | |
| 941 | 941 | return h | |
| 942 | 942 | } | |
| @@ -1043,7 +1043,7 @@ func (w *Workspace) previewRow(axis layoutAxis, slot Rect, dim, lit bool) []Plac | |||
| 1043 | 1043 | // the overview axis shows only its selected lane. In a horizontal overview, | |
| 1044 | 1044 | // tiles entirely outside the area are hidden. | |
| 1045 | 1045 | func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes bool, tiles []Placement, span int, sel Rect) []Placement { | |
| 1046 | - | z := w.overviewZoom() | |
| 1046 | + | z := w.overviewZoom(axis) | |
| 1047 | 1047 | scale := func(v int) int { return int(math.Round(float64(v) * z)) } | |
| 1048 | 1048 | width := scale(span) | |
| 1049 | 1049 | u := w.overviewArea() | |
| @@ -1051,9 +1051,13 @@ func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes | |||
| 1051 | 1051 | if axis == horizontalAxis { | |
| 1052 | 1052 | top = slot.Y + (slot.H-scale(w.Usable.H))/2 | |
| 1053 | 1053 | } | |
| 1054 | - | // Rows stay centred on the output like rows without a stash; the pile | |
| 1055 | - | // only pushes a row right when they would overlap. | |
| 1056 | - | left := w.pileWidth() | |
| 1054 | + | // Rows stay centred on the output like rows without a stash; in a | |
| 1055 | + | // vertical overview the pile only pushes a row right when they would | |
| 1056 | + | // overlap. A horizontal one draws the pile over the row. | |
| 1057 | + | left := 0 | |
| 1058 | + | if axis == verticalAxis { | |
| 1059 | + | left = w.pileWidth() | |
| 1060 | + | } | |
| 1057 | 1061 | x := max(slot.X+(slot.W-width)/2, slot.X+left) | |
| 1058 | 1062 | ux, uw := slot.X+left, slot.W-left | |
| 1059 | 1063 | if width > uw { | |
| @@ -53,10 +53,12 @@ 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 pinned to the left edge of | 56 | + | // A workspace's stash shows as a pile of cards: the selected stashed window |
| 57 | - | // its row: the selected stashed window in front at overviewCardZoom, up to | 57 | + | // in front at overviewCardZoom, up to overviewCards-1 others behind it, each |
| 58 | - | // overviewCards-1 others behind it, each overviewCardStep further up and | 58 | + | // overviewCardStep further up and right, dimmed. A vertical overview pins |
| 59 | - | // right, dimmed. The row takes the width the pile leaves. | 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. | |||
| 60 | const ( | 62 | const ( | ||
| 61 | overviewCardZoom = 0.2 | 63 | overviewCardZoom = 0.2 | ||
| 62 | overviewCards = 4 | 64 | overviewCards = 4 | ||
| @@ -423,15 +425,22 @@ func (m *Monitor) cycleCard(w *Workspace, d int) bool { | |||||
| 423 | // overviewLane is the vertical move of an overview whose workspaces sit side | 425 | // overviewLane is the vertical move of an overview whose workspaces sit side | ||
| 424 | // by side (cascade): a selected stash card cycles the pile; else the | 426 | // by side (cascade): a selected stash card cycles the pile; else the | ||
| 425 | // selection goes to the previous or next lane (band), at the closest column, | 427 | // selection goes to the previous or next lane (band), at the closest column, | ||
| 426 | - | // stopping at the first and last. Covering floats stack along the horizontal | 428 | + | // stopping at the first. Down past the last lane, or from a card that is not |
| 427 | - | // axis, not this one. | 429 | + | // the columns, enters the stash, which sits below. Covering floats stack |
| 430 | + | // along the horizontal axis, not this one. | |||
| 428 | func (m *Monitor) overviewLane(dy int) { | 431 | func (m *Monitor) overviewLane(dy int) { | ||
| 429 | w := m.Current() | 432 | w := m.Current() | ||
| 430 | - | if m.cycleCard(w, dy) || m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 { | 433 | + | if m.cycleCard(w, dy) { |
| 431 | return | 434 | return | ||
| 432 | } | 435 | } | ||
| 433 | - | if i := w.laneNeighbor(dy); i >= 0 { | 436 | + | if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 { |
| 434 | - | m.selectLaneColumn(w, i) | 437 | + | if i := w.laneNeighbor(dy); i >= 0 { |
| 438 | + | m.selectLaneColumn(w, i) | |||
| 439 | + | return | |||
| 440 | + | } | |||
| 441 | + | } | |||
| 442 | + | if dy > 0 && len(w.Stash) > 0 { | |||
| 443 | + | m.selectCard(w, w.stashAt) | |||
| 435 | } | 444 | } | ||
| 436 | } | 445 | } | ||
| 437 | 446 | ||||
| @@ -444,10 +453,11 @@ func (m *Monitor) selectLaneColumn(w *Workspace, i int) { | |||||
| 444 | 453 | ||||
| 445 | // overviewAcross is the horizontal move of an overview whose workspaces sit | 454 | // overviewAcross is the horizontal move of an overview whose workspaces sit | ||
| 446 | // side by side (cascade): a column inside the selected lane. Past the | 455 | // side by side (cascade): a column inside the selected lane. Past the | ||
| 447 | - | // lane's first column it goes to the stash, else to the previous workspace; | 456 | + | // lane's first column it goes to the previous workspace; past the last |
| 448 | - | // past the last column, to the next workspace. Covering floats fan along | 457 | + | // column, to the next workspace. From a stash card, left goes to the |
| 449 | - | // this axis and are traversed before the stash or a workspace: the card on | 458 | + | // previous workspace and right back to the columns. Covering floats fan |
| 450 | - | // the left is the one behind. | 459 | + | // along this axis and are traversed before a workspace: the card on the |
| 460 | + | // left is the one behind. | |||
| 451 | func (m *Monitor) overviewAcross(dx int) { | 461 | func (m *Monitor) overviewAcross(dx int) { | ||
| 452 | w := m.Current() | 462 | w := m.Current() | ||
| 453 | if m.cardAt(w) >= 0 { | 463 | if m.cardAt(w) >= 0 { | ||
| @@ -459,15 +469,18 @@ func (m *Monitor) overviewAcross(dx int) { | |||||
| 459 | } | 469 | } | ||
| 460 | m.clearCard() | 470 | m.clearCard() | ||
| 461 | m.ov.selected = 0 | 471 | m.ov.selected = 0 | ||
| 462 | - | if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 { | 472 | + | if len(w.Columns) > 0 { |
| 463 | - | // The first column of the focused lane. | 473 | + | // The columns come to the front, even when down entered the |
| 474 | + | // stash from a covering float: at the first column of the | |||
| 475 | + | // focused lane. | |||
| 476 | + | m.setFront(w, stackItem{kind: stackColumns}) | |||
| 464 | i, _ := w.laneBounds(w.laneOf(w.Focus)) | 477 | i, _ := w.laneBounds(w.laneOf(w.Focus)) | ||
| 465 | m.selectLaneColumn(w, i) | 478 | m.selectLaneColumn(w, i) | ||
| 466 | } | 479 | } | ||
| 467 | return | 480 | return | ||
| 468 | } | 481 | } | ||
| 469 | if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 { | 482 | if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 { | ||
| 470 | - | m.laneEdge(w, dx) | 483 | + | m.laneEdge(dx) |
| 471 | return | 484 | return | ||
| 472 | } | 485 | } | ||
| 473 | first, last := w.laneBounds(w.laneOf(w.Focus)) | 486 | first, last := w.laneBounds(w.laneOf(w.Focus)) | ||
| @@ -475,20 +488,15 @@ func (m *Monitor) overviewAcross(dx int) { | |||||
| 475 | m.selectLaneColumn(w, next) | 488 | m.selectLaneColumn(w, next) | ||
| 476 | return | 489 | return | ||
| 477 | } | 490 | } | ||
| 478 | - | m.laneEdge(w, dx) | 491 | + | m.laneEdge(dx) |
| 479 | } | 492 | } | ||
| 480 | 493 | ||||
| 481 | // laneEdge is a horizontal move off the edge of the selected lane, or off a | 494 | // laneEdge is a horizontal move off the edge of the selected lane, or off a | ||
| 482 | - | // card that is not the columns: the stack first, then the stash on the left | 495 | + | // card that is not the columns: the stack first, then a workspace. |
| 483 | - | // or a workspace. | 496 | + | func (m *Monitor) laneEdge(dx int) { |
| 484 | - | func (m *Monitor) laneEdge(w *Workspace, dx int) { | |||
| 485 | if m.moveStack(-dx) { | 497 | if m.moveStack(-dx) { | ||
| 486 | return | 498 | return | ||
| 487 | } | 499 | } | ||
| 488 | - | if dx < 0 && len(w.Stash) > 0 { | |||
| 489 | - | m.selectCard(w, w.stashAt) | |||
| 490 | - | return | |||
| 491 | - | } | |||
| 492 | m.overviewStep(dx) | 500 | m.overviewStep(dx) | ||
| 493 | } | 501 | } | ||
| 494 | 502 | ||||
| @@ -628,8 +636,8 @@ func (m *Monitor) overviewAxis() layoutAxis { return m.Current().policy().worksp | |||||
| 628 | // | 636 | // | ||
| 629 | // Vertical axis: full-width slots, the current one centred; the rule is | 637 | // Vertical axis: full-width slots, the current one centred; the rule is | ||
| 630 | // horizontal. Horizontal axis: full-height slots as wide as their content, | 638 | // horizontal. Horizontal axis: full-height slots as wide as their content, | ||
| 631 | - | // stash pile and stack peeks included, the current one centred; the rule is | 639 | + | // stack peeks included (the stash pile sits over the row), the current one |
| 632 | - | // vertical, in the spacing between two slots. | 640 | + | // centred; the rule is vertical, in the spacing between two slots. |
| 633 | func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | 641 | func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | ||
| 634 | cur := m.Current() | 642 | cur := m.Current() | ||
| 635 | u := cur.overviewArea() | 643 | u := cur.overviewArea() | ||
| @@ -637,13 +645,10 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | |||||
| 637 | axis := m.overviewAxis() | 645 | axis := m.overviewAxis() | ||
| 638 | var cs Rect | 646 | var cs Rect | ||
| 639 | if axis == horizontalAxis { | 647 | if axis == horizontalAxis { | ||
| 640 | - | // The row stays centred on the area; the pile extends the slot to | 648 | + | row := cur.rowWidth(axis) |
| 641 | - | // the left, pushing the previous workspace, unless it would leave | 649 | + | cs = Rect{X: u.X + (u.W-row)/2, Y: u.Y, W: row, H: u.H} |
| 642 | - | // the area. | |||
| 643 | - | pile, row := cur.pileWidth(), cur.rowWidth() | |||
| 644 | - | cs = Rect{X: max(u.X+(u.W-row)/2-pile, u.X), Y: u.Y, W: pile + row, H: u.H} | |||
| 645 | } else { | 650 | } else { | ||
| 646 | - | hc := cur.rowHeight() | 651 | + | hc := cur.rowHeight(axis) |
| 647 | cs = Rect{X: u.X, Y: u.Y + (u.H-hc)/2, W: u.W, H: hc} | 652 | cs = Rect{X: u.X, Y: u.Y + (u.H-hc)/2, W: u.W, H: hc} | ||
| 648 | } | 653 | } | ||
| 649 | rows := map[*Workspace]Rect{cur: cs} | 654 | rows := map[*Workspace]Rect{cur: cs} | ||
| @@ -673,7 +678,7 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | |||||
| 673 | // slot cs along axis, spacing away; gapAt is where the spacing begins. | 678 | // slot cs along axis, spacing away; gapAt is where the spacing begins. | ||
| 674 | func neighborSlot(axis layoutAxis, u, cs Rect, w *Workspace, before bool, spacing int) (slot Rect, gapAt int) { | 679 | func neighborSlot(axis layoutAxis, u, cs Rect, w *Workspace, before bool, spacing int) (slot Rect, gapAt int) { | ||
| 675 | if axis == horizontalAxis { | 680 | if axis == horizontalAxis { | ||
| 676 | - | slot = Rect{Y: u.Y, W: w.slotWidth(), H: u.H} | 681 | + | slot = Rect{Y: u.Y, W: w.rowWidth(axis), H: u.H} |
| 677 | if before { | 682 | if before { | ||
| 678 | gapAt = cs.X - spacing | 683 | gapAt = cs.X - spacing | ||
| 679 | slot.X = gapAt - slot.W | 684 | slot.X = gapAt - slot.W | ||
| @@ -683,7 +688,7 @@ func neighborSlot(axis layoutAxis, u, cs Rect, w *Workspace, before bool, spacin | |||||
| 683 | } | 688 | } | ||
| 684 | return slot, gapAt | 689 | return slot, gapAt | ||
| 685 | } | 690 | } | ||
| 686 | - | h := w.rowHeight() | 691 | + | h := w.rowHeight(axis) |
| 687 | slot = Rect{X: u.X, W: u.W, H: h} | 692 | slot = Rect{X: u.X, W: u.W, H: h} | ||
| 688 | if before { | 693 | if before { | ||
| 689 | gapAt = cs.Y - spacing | 694 | gapAt = cs.Y - spacing | ||
| @@ -718,7 +723,7 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement { | |||||
| 718 | at := m.cardAt(w) | 723 | at := m.cardAt(w) | ||
| 719 | result = append(result, m.stackRow(w, axis, slot, w != cur, at < 0)...) | 724 | result = append(result, m.stackRow(w, axis, slot, w != cur, at < 0)...) | ||
| 720 | if len(w.Stash) > 0 { | 725 | if len(w.Stash) > 0 { | ||
| 721 | - | result = append(result, w.pile(w.pileBand(axis, slot), w != cur, at)...) | 726 | + | result = append(result, w.pile(axis, slot, w != cur, at)...) |
| 722 | } | 727 | } | ||
| 723 | } | 728 | } | ||
| 724 | for _, f := range w.Floats { | 729 | for _, f := range w.Floats { | ||
| @@ -802,8 +807,8 @@ func fan(cards [][]Placement, front, maxBehind, maxBefore int, circular bool, | |||||
| 802 | return out | 807 | return out | ||
| 803 | } | 808 | } | ||
| 804 | 809 | ||||
| 805 | - | // pileWidth is the room the stash pile of w takes on the left of its row, | 810 | + | // pileWidth is the room the stash pile of w takes on the left of its row in |
| 806 | - | // its margin included; 0 without a stash. | 811 | + | // a vertical overview, its margin included; 0 without a stash. |
| 807 | func (w *Workspace) pileWidth() int { | 812 | func (w *Workspace) pileWidth() int { | ||
| 808 | if len(w.Stash) == 0 { | 813 | if len(w.Stash) == 0 { | ||
| 809 | return 0 | 814 | return 0 | ||
| @@ -820,11 +825,11 @@ func (w *Workspace) cardStep() int { | |||||
| 820 | return min(overviewCardStep, a.W/100, a.H/50) | 825 | return min(overviewCardStep, a.W/100, a.H/50) | ||
| 821 | } | 826 | } | ||
| 822 | 827 | ||||
| 823 | - | // pile places the stash of w as cards on the left of band (see pileBand): | 828 | + | // pile places the stash of w as cards in its slot along axis (see the |
| 824 | - | // entry front (the stash selection when -1) in | 829 | + | // overview's stash comment): entry front (the stash selection when -1) in |
| 825 | // front, the next ones behind it, dimmed, the rest hidden. Only a front card | 830 | // front, the next ones behind it, dimmed, the rest hidden. Only a front card | ||
| 826 | // of the current row (dim false) with front set is selected. | 831 | // of the current row (dim false) with front set is selected. | ||
| 827 | - | func (w *Workspace) pile(band Rect, dim bool, front int) []Placement { | 832 | + | func (w *Workspace) pile(axis layoutAxis, slot Rect, dim bool, front int) []Placement { |
| 828 | n := len(w.Stash) | 833 | n := len(w.Stash) | ||
| 829 | if n == 0 { | 834 | if n == 0 { | ||
| 830 | return nil | 835 | return nil | ||
| @@ -838,10 +843,19 @@ func (w *Workspace) pile(band Rect, dim bool, front int) []Placement { | |||||
| 838 | u := w.overviewArea() | 843 | u := w.overviewArea() | ||
| 839 | cw := int(math.Round(float64(r.W) * overviewCardZoom)) | 844 | cw := int(math.Round(float64(r.W) * overviewCardZoom)) | ||
| 840 | ch := int(math.Round(float64(r.H) * overviewCardZoom)) | 845 | ch := int(math.Round(float64(r.H) * overviewCardZoom)) | ||
| 841 | - | rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom())) | 846 | + | rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom(axis))) |
| 842 | - | // Stack rows reserve two peek steps above the front card. Align the | 847 | + | var x0, y0 int |
| 843 | - | // stash with that front band, not with the top of its envelope. | 848 | + | if axis == horizontalAxis { |
| 844 | - | x0, y0 := band.X+u.W/100, band.Y+(rowH-ch)/2 | 849 | + | // Centred on the row, across the bottom edge of the selected |
| 850 | + | // lane, which is centred in the slot. | |||
| 851 | + | x0 = slot.X + (slot.W-cw)/2 | |||
| 852 | + | y0 = slot.Y + (slot.H-rowH)/2 + rowH - ch/2 | |||
| 853 | + | } else { | |||
| 854 | + | // Stack rows reserve two peek steps above the front card. Align | |||
| 855 | + | // the stash with that front band, not with the top of its envelope. | |||
| 856 | + | band := w.frontSlot(axis, slot) | |||
| 857 | + | x0, y0 = band.X+u.W/100, band.Y+(rowH-ch)/2 | |||
| 858 | + | } | |||
| 845 | cards := make([][]Placement, n) | 859 | cards := make([][]Placement, n) | ||
| 846 | for i := range w.Stash { | 860 | for i := range w.Stash { | ||
| 847 | cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: Rect{X: x0, Y: y0, W: cw, H: ch}, Preview: overviewCardZoom, Focused: selected}} | 861 | cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: Rect{X: x0, Y: y0, W: cw, H: ch}, Preview: overviewCardZoom, Focused: selected}} | ||
| @@ -853,22 +867,26 @@ func (w *Workspace) pile(band Rect, dim bool, front int) []Placement { | |||||
| 853 | } | 867 | } | ||
| 854 | 868 | ||||
| 855 | // overviewZoom is the scale that fits every column of w in the usable | 869 | // overviewZoom is the scale that fits every column of w in the usable | ||
| 856 | - | // width, within the overview bounds. | 870 | + | // width, within the overview bounds, in an overview along axis: only a |
| 857 | - | func (w *Workspace) overviewZoom() float64 { | 871 | + | // vertical one sets the stash pile beside the row. |
| 858 | - | z, _ := w.zoomSpan() | 872 | + | func (w *Workspace) overviewZoom(axis layoutAxis) float64 { |
| 873 | + | z, _ := w.zoomSpan(axis) | |||
| 859 | return z | 874 | return z | ||
| 860 | } | 875 | } | ||
| 861 | 876 | ||||
| 862 | // zoomSpan is overviewZoom with the unscaled span of the tiles it fits. A | 877 | // zoomSpan is overviewZoom with the unscaled span of the tiles it fits. A | ||
| 863 | // cascade workspace spans one screen whatever its band count. | 878 | // cascade workspace spans one screen whatever its band count. | ||
| 864 | - | func (w *Workspace) zoomSpan() (float64, int) { | 879 | + | func (w *Workspace) zoomSpan(axis layoutAxis) (float64, int) { |
| 865 | // Only the span is read: the tiles go in the zoom scratch, dead on return. | 880 | // Only the span is read: the tiles go in the zoom scratch, dead on return. | ||
| 866 | var span int | 881 | var span int | ||
| 867 | w.zoomBuf, span, _ = w.previewTilesInto(w.zoomBuf[:0]) | 882 | w.zoomBuf, span, _ = w.previewTilesInto(w.zoomBuf[:0]) | ||
| 868 | if span <= 0 { | 883 | if span <= 0 { | ||
| 869 | return overviewMaxZoom, span | 884 | return overviewMaxZoom, span | ||
| 870 | } | 885 | } | ||
| 871 | - | room := float64(w.overviewArea().W-w.pileWidth()) * 0.96 | 886 | + | room := float64(w.overviewArea().W) * 0.96 |
| 887 | + | if axis == verticalAxis { | |||
| 888 | + | room = float64(w.overviewArea().W-w.pileWidth()) * 0.96 | |||
| 889 | + | } | |||
| 872 | z := min(max(room/float64(span), overviewMinZoom), overviewMaxZoom) | 890 | z := min(max(room/float64(span), overviewMinZoom), overviewMaxZoom) | ||
| 873 | if w.stacked() { | 891 | if w.stacked() { | ||
| 874 | // Reserve two clear peeks on either side of the front row. | 892 | // Reserve two clear peeks on either side of the front row. | ||
| @@ -877,31 +895,25 @@ func (w *Workspace) zoomSpan() (float64, int) { | |||||
| 877 | return z, span | 895 | return z, span | ||
| 878 | } | 896 | } | ||
| 879 | 897 | ||||
| 880 | - | // slotWidth is the width of the slot of w in a horizontal overview: its | 898 | + | // rowWidth is the width of the slot of w in a horizontal overview: what its |
| 881 | - | // stash pile and its row. | 899 | + | // tiles take (at most the area), plus the room for the covering floats |
| 882 | - | func (w *Workspace) slotWidth() int { | 900 | + | // peeking on either side of the front card. |
| 883 | - | return w.pileWidth() + w.rowWidth() | 901 | + | func (w *Workspace) rowWidth(axis layoutAxis) int { |
| 884 | - | } | 902 | + | z, span := w.zoomSpan(axis) |
| 885 | - | ||||
| 886 | - | // rowWidth is the width of the row of w in a horizontal overview: what its | |||
| 887 | - | // tiles take (at most what the pile leaves of the area), plus the room for | |||
| 888 | - | // the covering floats peeking on either side of the front card. | |||
| 889 | - | func (w *Workspace) rowWidth() int { | |||
| 890 | - | z, span := w.zoomSpan() | |||
| 891 | if span <= 0 { | 903 | if span <= 0 { | ||
| 892 | // An empty workspace keeps the room of a screen. | 904 | // An empty workspace keeps the room of a screen. | ||
| 893 | span = w.Usable.W | 905 | span = w.Usable.W | ||
| 894 | } | 906 | } | ||
| 895 | - | width := min(int(math.Round(float64(span)*z)), w.overviewArea().W-w.pileWidth()) | 907 | + | width := min(int(math.Round(float64(span)*z)), w.overviewArea().W) |
| 896 | if w.stacked() { | 908 | if w.stacked() { | ||
| 897 | - | width += 4 * w.peekStep() | 909 | + | width += 4 * w.peekStep(axis) |
| 898 | } | 910 | } | ||
| 899 | return width | 911 | return width | ||
| 900 | } | 912 | } | ||
| 901 | 913 | ||||
| 902 | // peekStep is the shared step for a stack row and its front-card inset. | 914 | // peekStep is the shared step for a stack row and its front-card inset. | ||
| 903 | - | func (w *Workspace) peekStep() int { | 915 | + | func (w *Workspace) peekStep(axis layoutAxis) int { |
| 904 | - | return max(1, int(math.Round(float64(w.Usable.H)*w.overviewZoom()*overviewPeekStep))) | 916 | + | return max(1, int(math.Round(float64(w.Usable.H)*w.overviewZoom(axis)*overviewPeekStep))) |
| 905 | } | 917 | } | ||
| 906 | 918 | ||||
| 907 | // frontSlot is the part of slot the front card of w takes: slot shifted | 919 | // frontSlot is the part of slot the front card of w takes: slot shifted | ||
| @@ -910,7 +922,7 @@ func (w *Workspace) peekStep() int { | |||||
| 910 | // the room of the two after it. A single-card row keeps slot. | 922 | // the room of the two after it. A single-card row keeps slot. | ||
| 911 | func (w *Workspace) frontSlot(axis layoutAxis, slot Rect) Rect { | 923 | func (w *Workspace) frontSlot(axis layoutAxis, slot Rect) Rect { | ||
| 912 | if w.stacked() { | 924 | if w.stacked() { | ||
| 913 | - | p := w.peekStep() | 925 | + | p := w.peekStep(axis) |
| 914 | axis.offset(&slot, 2*p) | 926 | axis.offset(&slot, 2*p) | ||
| 915 | if axis == horizontalAxis { | 927 | if axis == horizontalAxis { | ||
| 916 | slot.W -= 4 * p | 928 | slot.W -= 4 * p | ||
| @@ -919,24 +931,12 @@ func (w *Workspace) frontSlot(axis layoutAxis, slot Rect) Rect { | |||||
| 919 | return slot | 931 | return slot | ||
| 920 | } | 932 | } | ||
| 921 | 933 | ||||
| 922 | - | // pileBand is the band the stash pile of w aligns with in its slot. A | |||
| 923 | - | // vertical overview aligns it with the front card (see frontSlot). A | |||
| 924 | - | // horizontal one puts it level with the selected lane, left of the whole | |||
| 925 | - | // stack of cards. | |||
| 926 | - | func (w *Workspace) pileBand(axis layoutAxis, slot Rect) Rect { | |||
| 927 | - | if axis == verticalAxis { | |||
| 928 | - | return w.frontSlot(axis, slot) | |||
| 929 | - | } | |||
| 930 | - | slot.Y += (slot.H - int(math.Round(float64(w.Usable.H)*w.overviewZoom()))) / 2 | |||
| 931 | - | return slot | |||
| 932 | - | } | |||
| 933 | - | ||||
| 934 | // rowHeight includes the largest possible stack envelope (two peeks above | 934 | // rowHeight includes the largest possible stack envelope (two peeks above | ||
| 935 | // and two below). Neighbor rows use the same measure as the current row. | 935 | // and two below). Neighbor rows use the same measure as the current row. | ||
| 936 | - | func (w *Workspace) rowHeight() int { | 936 | + | func (w *Workspace) rowHeight(axis layoutAxis) int { |
| 937 | - | h := int(math.Round(float64(w.Usable.H) * w.overviewZoom())) | 937 | + | h := int(math.Round(float64(w.Usable.H) * w.overviewZoom(axis))) |
| 938 | if w.stacked() { | 938 | if w.stacked() { | ||
| 939 | - | h += 4 * w.peekStep() | 939 | + | h += 4 * w.peekStep(axis) |
| 940 | } | 940 | } | ||
| 941 | return h | 941 | return h | ||
| 942 | } | 942 | } | ||
| @@ -1043,7 +1043,7 @@ func (w *Workspace) previewRow(axis layoutAxis, slot Rect, dim, lit bool) []Plac | |||||
| 1043 | // the overview axis shows only its selected lane. In a horizontal overview, | 1043 | // the overview axis shows only its selected lane. In a horizontal overview, | ||
| 1044 | // tiles entirely outside the area are hidden. | 1044 | // tiles entirely outside the area are hidden. | ||
| 1045 | func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes bool, tiles []Placement, span int, sel Rect) []Placement { | 1045 | func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes bool, tiles []Placement, span int, sel Rect) []Placement { | ||
| 1046 | - | z := w.overviewZoom() | 1046 | + | z := w.overviewZoom(axis) |
| 1047 | scale := func(v int) int { return int(math.Round(float64(v) * z)) } | 1047 | scale := func(v int) int { return int(math.Round(float64(v) * z)) } | ||
| 1048 | width := scale(span) | 1048 | width := scale(span) | ||
| 1049 | u := w.overviewArea() | 1049 | u := w.overviewArea() | ||
| @@ -1051,9 +1051,13 @@ func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes | |||||
| 1051 | if axis == horizontalAxis { | 1051 | if axis == horizontalAxis { | ||
| 1052 | top = slot.Y + (slot.H-scale(w.Usable.H))/2 | 1052 | top = slot.Y + (slot.H-scale(w.Usable.H))/2 | ||
| 1053 | } | 1053 | } | ||
| 1054 | - | // Rows stay centred on the output like rows without a stash; the pile | 1054 | + | // Rows stay centred on the output like rows without a stash; in a |
| 1055 | - | // only pushes a row right when they would overlap. | 1055 | + | // vertical overview the pile only pushes a row right when they would |
| 1056 | - | left := w.pileWidth() | 1056 | + | // overlap. A horizontal one draws the pile over the row. |
| 1057 | + | left := 0 | |||
| 1058 | + | if axis == verticalAxis { | |||
| 1059 | + | left = w.pileWidth() | |||
| 1060 | + | } | |||
| 1057 | x := max(slot.X+(slot.W-width)/2, slot.X+left) | 1061 | x := max(slot.X+(slot.W-width)/2, slot.X+left) | ||
| 1058 | ux, uw := slot.X+left, slot.W-left | 1062 | ux, uw := slot.X+left, slot.W-left | ||
| 1059 | if width > uw { | 1063 | if width > uw { | ||
M internal/core/overview_behavior_test.go +5 -4
| @@ -50,12 +50,13 @@ func TestOverviewPeekEnvelopeAndSingleCardHorizontal(t *testing.T) { | |||
| 50 | 50 | front := previewOf(t, m.Layout(), 7) | |
| 51 | 51 | back := previewOf(t, m.Layout(), 8) | |
| 52 | 52 | step := float64(front.Rect.Y - back.Rect.Y) | |
| 53 | - | if step < float64(w.Usable.H)*w.overviewZoom()*0.06 { | |
| 53 | + | if step < float64(w.Usable.H)*w.overviewZoom(verticalAxis)*0.06 { | |
| 54 | 54 | t.Fatalf("peek step %.1f", step) | |
| 55 | 55 | } | |
| 56 | - | y := w.Usable.Y + (w.Usable.H-w.rowHeight())/2 | |
| 57 | - | if back.Rect.Y < y || front.Rect.Y+front.Rect.H > y+w.rowHeight() { | |
| 58 | - | t.Fatalf("outside row front %+v back %+v row %d..%d", front, back, y, y+w.rowHeight()) | |
| 56 | + | h := w.rowHeight(verticalAxis) | |
| 57 | + | y := w.Usable.Y + (w.Usable.H-h)/2 | |
| 58 | + | if back.Rect.Y < y || front.Rect.Y+front.Rect.H > y+h { | |
| 59 | + | t.Fatalf("outside row front %+v back %+v row %d..%d", front, back, y, y+h) | |
| 59 | 60 | } | |
| 60 | 61 | m.OverviewMove(1, 0) | |
| 61 | 62 | if m.stackFront(w) != (stackItem{stackFloat, 7}) { | |
| @@ -50,12 +50,13 @@ func TestOverviewPeekEnvelopeAndSingleCardHorizontal(t *testing.T) { | |||||
| 50 | front := previewOf(t, m.Layout(), 7) | 50 | front := previewOf(t, m.Layout(), 7) | ||
| 51 | back := previewOf(t, m.Layout(), 8) | 51 | back := previewOf(t, m.Layout(), 8) | ||
| 52 | step := float64(front.Rect.Y - back.Rect.Y) | 52 | step := float64(front.Rect.Y - back.Rect.Y) | ||
| 53 | - | if step < float64(w.Usable.H)*w.overviewZoom()*0.06 { | 53 | + | if step < float64(w.Usable.H)*w.overviewZoom(verticalAxis)*0.06 { |
| 54 | t.Fatalf("peek step %.1f", step) | 54 | t.Fatalf("peek step %.1f", step) | ||
| 55 | } | 55 | } | ||
| 56 | - | y := w.Usable.Y + (w.Usable.H-w.rowHeight())/2 | 56 | + | h := w.rowHeight(verticalAxis) |
| 57 | - | if back.Rect.Y < y || front.Rect.Y+front.Rect.H > y+w.rowHeight() { | 57 | + | y := w.Usable.Y + (w.Usable.H-h)/2 |
| 58 | - | t.Fatalf("outside row front %+v back %+v row %d..%d", front, back, y, y+w.rowHeight()) | 58 | + | if back.Rect.Y < y || front.Rect.Y+front.Rect.H > y+h { |
| 59 | + | t.Fatalf("outside row front %+v back %+v row %d..%d", front, back, y, y+h) | |||
| 59 | } | 60 | } | ||
| 60 | m.OverviewMove(1, 0) | 61 | m.OverviewMove(1, 0) | ||
| 61 | if m.stackFront(w) != (stackItem{stackFloat, 7}) { | 62 | if m.stackFront(w) != (stackItem{stackFloat, 7}) { | ||
M internal/core/overview_cascade_test.go +124 -36
| @@ -2,6 +2,7 @@ package core | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | + | "math" | |
| 5 | 6 | "slices" | |
| 6 | 7 | "testing" | |
| 7 | 8 | ||
| @@ -70,7 +71,7 @@ func hiddenOff(p Placement) bool { return p.Hidden && p.Preview == 0 && !p.Focus | |||
| 70 | 71 | ||
| 71 | 72 | // The zoom does not depend on the number of bands. | |
| 72 | 73 | func TestOverviewCascadeZoomIgnoresBandCount(t *testing.T) { | |
| 73 | - | if one, three := cascadeWorkspace(3).overviewZoom(), cascadeWorkspace(7).overviewZoom(); one != three || one != overviewMaxZoom { | |
| 74 | + | if one, three := cascadeWorkspace(3).overviewZoom(horizontalAxis), cascadeWorkspace(7).overviewZoom(horizontalAxis); one != three || one != overviewMaxZoom { | |
| 74 | 75 | t.Fatalf("zoom with one band %v, with three %v", one, three) | |
| 75 | 76 | } | |
| 76 | 77 | } | |
| @@ -216,31 +217,40 @@ func TestOverviewCascadeNeighborBelowScrollShowsOneBand(t *testing.T) { | |||
| 216 | 217 | } | |
| 217 | 218 | } | |
| 218 | 219 | ||
| 219 | - | // The stash pile is inside the current slot, left of the band; the | |
| 220 | - | // previous workspace stays clear of it. | |
| 220 | + | // The stash pile is centred on the current row, straddling the bottom edge | |
| 221 | + | // of the selected lane, over the tiles; it takes no room in the slot. | |
| 221 | 222 | func TestOverviewCascadeStashInSlot(t *testing.T) { | |
| 222 | 223 | m := cascadeOverview() | |
| 224 | + | m.ToggleOverview() | |
| 225 | + | bare, _ := m.overviewRows() | |
| 226 | + | m.CancelOverview() | |
| 223 | 227 | withStash(m, 2) | |
| 224 | 228 | m.ToggleOverview() | |
| 225 | 229 | rows, _ := m.overviewRows() | |
| 226 | - | cur, prev := rows[m.Current()], rows[m.Workspaces[0]] | |
| 230 | + | w := m.Current() | |
| 231 | + | cur, prev := rows[w], rows[m.Workspaces[0]] | |
| 232 | + | if cur != bare[w] { | |
| 233 | + | t.Fatalf("the pile changed the slot: %v, without it %v", cur, bare[w]) | |
| 234 | + | } | |
| 227 | 235 | ps := m.Layout() | |
| 228 | - | left := cur.X + cur.W | |
| 229 | - | for _, id := range []WindowID{7, 8, 9} { | |
| 230 | - | left = min(left, previewOf(t, ps, id).Rect.X) | |
| 236 | + | lane := previewOf(t, ps, 8).Rect | |
| 237 | + | // 41 is the front card of the pile, 40 behind it. | |
| 238 | + | front := previewOf(t, ps, 41) | |
| 239 | + | if front.Hidden || front.Peek || front.Preview != overviewCardZoom { | |
| 240 | + | t.Fatalf("front card: %+v", front) | |
| 231 | 241 | } | |
| 232 | - | for _, id := range []WindowID{40, 41} { | |
| 233 | - | p := previewOf(t, ps, id) | |
| 234 | - | if p.Hidden || p.Preview != overviewCardZoom { | |
| 235 | - | t.Fatalf("pile card %d: %+v", id, p) | |
| 236 | - | } | |
| 237 | - | if p.Rect.X < cur.X || p.Rect.X+p.Rect.W > left { | |
| 238 | - | t.Fatalf("pile card %d not inside the slot, left of the band (%d..%d): %+v", id, cur.X, left, p.Rect) | |
| 239 | - | } | |
| 240 | - | // 41 is the front card of the pile. | |
| 241 | - | if id == 41 && !within1(centreY(p.Rect), centreY(previewOf(t, ps, 8).Rect)) { | |
| 242 | - | t.Fatalf("pile not level with the selected lane: %+v", p.Rect) | |
| 243 | - | } | |
| 242 | + | if back := previewOf(t, ps, 40); back.Hidden || !back.Peek { | |
| 243 | + | t.Fatalf("card behind: %+v", back) | |
| 244 | + | } | |
| 245 | + | 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) | |
| 247 | + | } | |
| 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) | |
| 244 | 254 | } | |
| 245 | 255 | // The pile does not shift the band: its row stays centred on the area. | |
| 246 | 256 | u := m.Current().overviewArea() | |
| @@ -248,11 +258,6 @@ func TestOverviewCascadeStashInSlot(t *testing.T) { | |||
| 248 | 258 | if mid := (lo.X + hi.X + hi.W) / 2; !within1(mid, u.X+u.W/2) { | |
| 249 | 259 | t.Fatalf("band centre %d with a stash, area centre %d", mid, u.X+u.W/2) | |
| 250 | 260 | } | |
| 251 | - | for _, id := range []WindowID{1, 2, 3} { | |
| 252 | - | if p := previewOf(t, ps, id); !p.Hidden && p.Rect.X+p.Rect.W > cur.X { | |
| 253 | - | t.Fatalf("previous workspace overlaps the slot with the pile: %+v", p.Rect) | |
| 254 | - | } | |
| 255 | - | } | |
| 256 | 261 | if prev.X+prev.W > cur.X { | |
| 257 | 262 | t.Fatalf("slots overlap: %v %v", prev, cur) | |
| 258 | 263 | } | |
| @@ -334,21 +339,32 @@ func TestOverviewCascadeKeys(t *testing.T) { | |||
| 334 | 339 | } | |
| 335 | 340 | m.CancelOverview() | |
| 336 | 341 | ||
| 337 | - | // With a stash: left from the first column enters it, right | |
| 338 | - | // comes back to the band's first column, left twice goes to | |
| 339 | - | // the previous workspace. | |
| 342 | + | // With a stash: left from the first column still goes to the | |
| 343 | + | // 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. | |
| 340 | 346 | withStash(m, 2) | |
| 341 | 347 | w.FocusID(7) | |
| 342 | 348 | m.ToggleOverview() | |
| 343 | 349 | d.move(m, -1, 0) | |
| 350 | + | if m.Current() != prev { | |
| 351 | + | t.Fatal("left from the first column with a stash did not reach the previous workspace") | |
| 352 | + | } | |
| 353 | + | m.CancelOverview() | |
| 354 | + | m.ToggleOverview() | |
| 355 | + | d.move(m, 0, 1) | |
| 356 | + | if m.cardAt(w) >= 0 || sel() != 10 { | |
| 357 | + | t.Fatalf("down to the last band: card %d, column %d", m.cardAt(w), sel()) | |
| 358 | + | } | |
| 359 | + | d.move(m, 0, 1) | |
| 344 | 360 | if m.cardAt(w) < 0 { | |
| 345 | - | t.Fatal("left from the first column did not select the stash") | |
| 361 | + | t.Fatal("down past the last band did not select the stash") | |
| 346 | 362 | } | |
| 347 | 363 | d.move(m, 1, 0) | |
| 348 | - | if m.cardAt(w) >= 0 || sel() != 7 { | |
| 364 | + | if m.cardAt(w) >= 0 || sel() != 10 { | |
| 349 | 365 | t.Fatalf("right from the stash: card %d, column %d", m.cardAt(w), sel()) | |
| 350 | 366 | } | |
| 351 | - | d.move(m, -1, 0) | |
| 367 | + | d.move(m, 0, 1) | |
| 352 | 368 | d.move(m, -1, 0) | |
| 353 | 369 | if m.Current() != prev { | |
| 354 | 370 | t.Fatal("left from the stash did not reach the previous workspace") | |
| @@ -369,9 +385,9 @@ func TestOverviewCascadeStashPile(t *testing.T) { | |||
| 369 | 385 | m := cascadeOverview() | |
| 370 | 386 | withStash(m, 3) | |
| 371 | 387 | w := m.Current() | |
| 372 | - | w.FocusID(7) | |
| 388 | + | w.FocusID(10) | |
| 373 | 389 | m.ToggleOverview() | |
| 374 | - | d.move(m, -1, 0) | |
| 390 | + | d.move(m, 0, 1) | |
| 375 | 391 | if m.cardAt(w) != w.stashAt { | |
| 376 | 392 | t.Fatalf("entered at %d", m.cardAt(w)) | |
| 377 | 393 | } | |
| @@ -467,6 +483,23 @@ func TestOverviewCascadeThreeFinger(t *testing.T) { | |||
| 467 | 483 | } | |
| 468 | 484 | } | |
| 469 | 485 | ||
| 486 | + | // Three fingers down past the last band enter the stash below it. | |
| 487 | + | func TestOverviewCascadeThreeFingerDownEntersStash(t *testing.T) { | |
| 488 | + | c, m := cascadeCore(t) | |
| 489 | + | withStash(m, 2) | |
| 490 | + | w := m.Current() | |
| 491 | + | w.FocusID(6) | |
| 492 | + | m.ToggleOverview() | |
| 493 | + | c.swipeBegin(ports.SwipeBegin{Fingers: 3}) | |
| 494 | + | for range 4 { | |
| 495 | + | c.swipeUpdate(ports.SwipeUpdate{DY: 40}) | |
| 496 | + | } | |
| 497 | + | c.swipeEnd(ports.SwipeEnd{}) | |
| 498 | + | if m.Current() != w || m.cardAt(w) != w.stashAt { | |
| 499 | + | t.Fatalf("swipe down from the last band: card %d, want %d", m.cardAt(w), w.stashAt) | |
| 500 | + | } | |
| 501 | + | } | |
| 502 | + | ||
| 470 | 503 | // Right from a stash card lands on the maximized column, which keeps its | |
| 471 | 504 | // maximization: it is alone in its band. | |
| 472 | 505 | func TestOverviewCascadeStashToMaximized(t *testing.T) { | |
| @@ -505,6 +538,61 @@ func TestOverviewCascadeNamedDivider(t *testing.T) { | |||
| 505 | 538 | } | |
| 506 | 539 | } | |
| 507 | 540 | ||
| 541 | + | // A stash takes no room in a cascade overview, on a neighbor either: a | |
| 542 | + | // wide scroll workspace beside it keeps its zoom and slot. | |
| 543 | + | func TestOverviewCascadeNeighborStashKeepsSlot(t *testing.T) { | |
| 544 | + | m := cascadeOverview() | |
| 545 | + | prev := m.Workspaces[0] | |
| 546 | + | m.Focus(0) | |
| 547 | + | for id := WindowID(60); id < 64; id++ { | |
| 548 | + | m.AddWindow(id) | |
| 549 | + | } | |
| 550 | + | m.Focus(1) | |
| 551 | + | m.ToggleOverview() | |
| 552 | + | bare, _ := m.overviewRows() | |
| 553 | + | zoom := prev.overviewZoom(horizontalAxis) | |
| 554 | + | m.CancelOverview() | |
| 555 | + | if zoom <= overviewMinZoom || zoom >= overviewMaxZoom { | |
| 556 | + | t.Fatalf("setup: neighbor zoom %v is at a bound", zoom) | |
| 557 | + | } | |
| 558 | + | // The same columns, with one more window stashed. | |
| 559 | + | m.Focus(0) | |
| 560 | + | m.AddWindow(64) | |
| 561 | + | prev.FocusID(64) | |
| 562 | + | prev.ToggleWindowStash() | |
| 563 | + | m.Focus(1) | |
| 564 | + | m.ToggleOverview() | |
| 565 | + | rows, _ := m.overviewRows() | |
| 566 | + | if rows[prev] != bare[prev] || prev.overviewZoom(horizontalAxis) != zoom { | |
| 567 | + | t.Fatalf("neighbor slot %v zoom %v with a stash, %v %v without", rows[prev], prev.overviewZoom(horizontalAxis), bare[prev], zoom) | |
| 568 | + | } | |
| 569 | + | if p := previewOf(t, m.Layout(), 64); p.Hidden || !p.Peek { | |
| 570 | + | t.Fatalf("neighbor pile card: %+v", p) | |
| 571 | + | } | |
| 572 | + | } | |
| 573 | + | ||
| 574 | + | // Down from a covering float enters the stash; right brings the columns | |
| 575 | + | // back to the front. | |
| 576 | + | func TestOverviewCascadeFloatStashRight(t *testing.T) { | |
| 577 | + | m := cascadeOverview() | |
| 578 | + | withStash(m, 1) | |
| 579 | + | w := m.Current() | |
| 580 | + | w.AddFloating(90, 300, 200) | |
| 581 | + | w.FocusID(90) | |
| 582 | + | m.ToggleOverview() | |
| 583 | + | if f := m.stackFront(w); f.kind != stackFloat { | |
| 584 | + | t.Fatalf("setup: front %+v", f) | |
| 585 | + | } | |
| 586 | + | m.OverviewMove(0, 1) | |
| 587 | + | if m.cardAt(w) < 0 { | |
| 588 | + | t.Fatal("down from the float did not select the stash") | |
| 589 | + | } | |
| 590 | + | m.OverviewMove(1, 0) | |
| 591 | + | if m.cardAt(w) >= 0 || m.stackFront(w).kind != stackColumns || m.overviewTarget() == 0 { | |
| 592 | + | t.Fatalf("right from the stash: card %d, front %+v, target %d", m.cardAt(w), m.stackFront(w), m.overviewTarget()) | |
| 593 | + | } | |
| 594 | + | } | |
| 595 | + | ||
| 508 | 596 | // Covering floats fan along the horizontal axis: left goes deeper in the | |
| 509 | 597 | // stack (the cards behind show on the left), right comes back to the front. | |
| 510 | 598 | func TestOverviewCascadeCoveringFloat(t *testing.T) { | |
| @@ -553,8 +641,8 @@ func TestOverviewCascadeCoveringFloat(t *testing.T) { | |||
| 553 | 641 | // The cards behind fan to the left: coming to the front, the | |
| 554 | 642 | // columns move right by one peek step. | |
| 555 | 643 | front := previewOf(t, m.Layout(), 10) | |
| 556 | - | if front.Peek || front.Rect.X-behind.Rect.X != w.peekStep() { | |
| 557 | - | t.Fatalf("columns behind %v, in front %v, peek step %d", behind.Rect, front.Rect, w.peekStep()) | |
| 644 | + | if front.Peek || front.Rect.X-behind.Rect.X != w.peekStep(horizontalAxis) { | |
| 645 | + | t.Fatalf("columns behind %v, in front %v, peek step %d", behind.Rect, front.Rect, w.peekStep(horizontalAxis)) | |
| 558 | 646 | } | |
| 559 | 647 | d.move(m, 1, 0) | |
| 560 | 648 | if f := m.stackFront(w); f.kind != stackFloat || f.id != 90 { | |
| @@ -617,9 +705,9 @@ func TestOverviewCascadeFocusBinds(t *testing.T) { | |||
| 617 | 705 | ||
| 618 | 706 | // A selected stash card keeps the pile cycle. | |
| 619 | 707 | withStash(m, 3) | |
| 620 | - | w.FocusID(7) | |
| 708 | + | w.FocusID(10) | |
| 621 | 709 | m.ToggleOverview() | |
| 622 | - | m.OverviewMove(-1, 0) | |
| 710 | + | m.OverviewMove(0, 1) | |
| 623 | 711 | at := m.cardAt(w) | |
| 624 | 712 | m.overviewFocus(ActionFocusWorkspacePrev) | |
| 625 | 713 | if m.Current() != w || m.cardAt(w) != at-1 { | |
| @@ -2,6 +2,7 @@ package core | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | + | "math" | |||
| 5 | "slices" | 6 | "slices" | ||
| 6 | "testing" | 7 | "testing" | ||
| 7 | 8 | ||||
| @@ -70,7 +71,7 @@ func hiddenOff(p Placement) bool { return p.Hidden && p.Preview == 0 && !p.Focus | |||||
| 70 | 71 | ||||
| 71 | // The zoom does not depend on the number of bands. | 72 | // The zoom does not depend on the number of bands. | ||
| 72 | func TestOverviewCascadeZoomIgnoresBandCount(t *testing.T) { | 73 | func TestOverviewCascadeZoomIgnoresBandCount(t *testing.T) { | ||
| 73 | - | if one, three := cascadeWorkspace(3).overviewZoom(), cascadeWorkspace(7).overviewZoom(); one != three || one != overviewMaxZoom { | 74 | + | if one, three := cascadeWorkspace(3).overviewZoom(horizontalAxis), cascadeWorkspace(7).overviewZoom(horizontalAxis); one != three || one != overviewMaxZoom { |
| 74 | t.Fatalf("zoom with one band %v, with three %v", one, three) | 75 | t.Fatalf("zoom with one band %v, with three %v", one, three) | ||
| 75 | } | 76 | } | ||
| 76 | } | 77 | } | ||
| @@ -216,31 +217,40 @@ func TestOverviewCascadeNeighborBelowScrollShowsOneBand(t *testing.T) { | |||||
| 216 | } | 217 | } | ||
| 217 | } | 218 | } | ||
| 218 | 219 | ||||
| 219 | - | // The stash pile is inside the current slot, left of the band; the | 220 | + | // The stash pile is centred on the current row, straddling the bottom edge |
| 220 | - | // previous workspace stays clear of it. | 221 | + | // of the selected lane, over the tiles; it takes no room in the slot. |
| 221 | func TestOverviewCascadeStashInSlot(t *testing.T) { | 222 | func TestOverviewCascadeStashInSlot(t *testing.T) { | ||
| 222 | m := cascadeOverview() | 223 | m := cascadeOverview() | ||
| 224 | + | m.ToggleOverview() | |||
| 225 | + | bare, _ := m.overviewRows() | |||
| 226 | + | m.CancelOverview() | |||
| 223 | withStash(m, 2) | 227 | withStash(m, 2) | ||
| 224 | m.ToggleOverview() | 228 | m.ToggleOverview() | ||
| 225 | rows, _ := m.overviewRows() | 229 | rows, _ := m.overviewRows() | ||
| 226 | - | cur, prev := rows[m.Current()], rows[m.Workspaces[0]] | 230 | + | w := m.Current() |
| 231 | + | cur, prev := rows[w], rows[m.Workspaces[0]] | |||
| 232 | + | if cur != bare[w] { | |||
| 233 | + | t.Fatalf("the pile changed the slot: %v, without it %v", cur, bare[w]) | |||
| 234 | + | } | |||
| 227 | ps := m.Layout() | 235 | ps := m.Layout() | ||
| 228 | - | left := cur.X + cur.W | 236 | + | lane := previewOf(t, ps, 8).Rect |
| 229 | - | for _, id := range []WindowID{7, 8, 9} { | 237 | + | // 41 is the front card of the pile, 40 behind it. |
| 230 | - | left = min(left, previewOf(t, ps, id).Rect.X) | 238 | + | front := previewOf(t, ps, 41) |
| 239 | + | if front.Hidden || front.Peek || front.Preview != overviewCardZoom { | |||
| 240 | + | t.Fatalf("front card: %+v", front) | |||
| 231 | } | 241 | } | ||
| 232 | - | for _, id := range []WindowID{40, 41} { | 242 | + | if back := previewOf(t, ps, 40); back.Hidden || !back.Peek { |
| 233 | - | p := previewOf(t, ps, id) | 243 | + | t.Fatalf("card behind: %+v", back) |
| 234 | - | if p.Hidden || p.Preview != overviewCardZoom { | 244 | + | } |
| 235 | - | t.Fatalf("pile card %d: %+v", id, p) | 245 | + | if !within1(front.Rect.X+front.Rect.W/2, cur.X+cur.W/2) { |
| 236 | - | } | 246 | + | t.Fatalf("pile not centred on the row %v: %+v", cur, front.Rect) |
| 237 | - | if p.Rect.X < cur.X || p.Rect.X+p.Rect.W > left { | 247 | + | } |
| 238 | - | t.Fatalf("pile card %d not inside the slot, left of the band (%d..%d): %+v", id, cur.X, left, p.Rect) | 248 | + | // The lane is centred in the slot; its tiles stop a gap short of its |
| 239 | - | } | 249 | + | // bottom edge. |
| 240 | - | // 41 is the front card of the pile. | 250 | + | rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom(horizontalAxis))) |
| 241 | - | if id == 41 && !within1(centreY(p.Rect), centreY(previewOf(t, ps, 8).Rect)) { | 251 | + | bottom := cur.Y + (cur.H-rowH)/2 + rowH |
| 242 | - | t.Fatalf("pile not level with the selected lane: %+v", p.Rect) | 252 | + | if !within1(centreY(front.Rect), bottom) || front.Rect.Y >= lane.Y+lane.H { |
| 243 | - | } | 253 | + | t.Fatalf("pile not across the lane's bottom edge %d (lane %v): %+v", bottom, lane, front.Rect) |
| 244 | } | 254 | } | ||
| 245 | // The pile does not shift the band: its row stays centred on the area. | 255 | // The pile does not shift the band: its row stays centred on the area. | ||
| 246 | u := m.Current().overviewArea() | 256 | u := m.Current().overviewArea() | ||
| @@ -248,11 +258,6 @@ func TestOverviewCascadeStashInSlot(t *testing.T) { | |||||
| 248 | if mid := (lo.X + hi.X + hi.W) / 2; !within1(mid, u.X+u.W/2) { | 258 | if mid := (lo.X + hi.X + hi.W) / 2; !within1(mid, u.X+u.W/2) { | ||
| 249 | t.Fatalf("band centre %d with a stash, area centre %d", mid, u.X+u.W/2) | 259 | t.Fatalf("band centre %d with a stash, area centre %d", mid, u.X+u.W/2) | ||
| 250 | } | 260 | } | ||
| 251 | - | for _, id := range []WindowID{1, 2, 3} { | |||
| 252 | - | if p := previewOf(t, ps, id); !p.Hidden && p.Rect.X+p.Rect.W > cur.X { | |||
| 253 | - | t.Fatalf("previous workspace overlaps the slot with the pile: %+v", p.Rect) | |||
| 254 | - | } | |||
| 255 | - | } | |||
| 256 | if prev.X+prev.W > cur.X { | 261 | if prev.X+prev.W > cur.X { | ||
| 257 | t.Fatalf("slots overlap: %v %v", prev, cur) | 262 | t.Fatalf("slots overlap: %v %v", prev, cur) | ||
| 258 | } | 263 | } | ||
| @@ -334,21 +339,32 @@ func TestOverviewCascadeKeys(t *testing.T) { | |||||
| 334 | } | 339 | } | ||
| 335 | m.CancelOverview() | 340 | m.CancelOverview() | ||
| 336 | 341 | ||||
| 337 | - | // With a stash: left from the first column enters it, right | 342 | + | // With a stash: left from the first column still goes to the |
| 338 | - | // comes back to the band's first column, left twice goes to | 343 | + | // previous workspace. Down past the last band enters the |
| 339 | - | // the previous workspace. | 344 | + | // stash, right comes back to the band's first column, and |
| 345 | + | // left from the stash goes to the previous workspace. | |||
| 340 | withStash(m, 2) | 346 | withStash(m, 2) | ||
| 341 | w.FocusID(7) | 347 | w.FocusID(7) | ||
| 342 | m.ToggleOverview() | 348 | m.ToggleOverview() | ||
| 343 | d.move(m, -1, 0) | 349 | d.move(m, -1, 0) | ||
| 350 | + | if m.Current() != prev { | |||
| 351 | + | t.Fatal("left from the first column with a stash did not reach the previous workspace") | |||
| 352 | + | } | |||
| 353 | + | m.CancelOverview() | |||
| 354 | + | m.ToggleOverview() | |||
| 355 | + | d.move(m, 0, 1) | |||
| 356 | + | if m.cardAt(w) >= 0 || sel() != 10 { | |||
| 357 | + | t.Fatalf("down to the last band: card %d, column %d", m.cardAt(w), sel()) | |||
| 358 | + | } | |||
| 359 | + | d.move(m, 0, 1) | |||
| 344 | if m.cardAt(w) < 0 { | 360 | if m.cardAt(w) < 0 { | ||
| 345 | - | t.Fatal("left from the first column did not select the stash") | 361 | + | t.Fatal("down past the last band did not select the stash") |
| 346 | } | 362 | } | ||
| 347 | d.move(m, 1, 0) | 363 | d.move(m, 1, 0) | ||
| 348 | - | if m.cardAt(w) >= 0 || sel() != 7 { | 364 | + | if m.cardAt(w) >= 0 || sel() != 10 { |
| 349 | t.Fatalf("right from the stash: card %d, column %d", m.cardAt(w), sel()) | 365 | t.Fatalf("right from the stash: card %d, column %d", m.cardAt(w), sel()) | ||
| 350 | } | 366 | } | ||
| 351 | - | d.move(m, -1, 0) | 367 | + | d.move(m, 0, 1) |
| 352 | d.move(m, -1, 0) | 368 | d.move(m, -1, 0) | ||
| 353 | if m.Current() != prev { | 369 | if m.Current() != prev { | ||
| 354 | t.Fatal("left from the stash did not reach the previous workspace") | 370 | t.Fatal("left from the stash did not reach the previous workspace") | ||
| @@ -369,9 +385,9 @@ func TestOverviewCascadeStashPile(t *testing.T) { | |||||
| 369 | m := cascadeOverview() | 385 | m := cascadeOverview() | ||
| 370 | withStash(m, 3) | 386 | withStash(m, 3) | ||
| 371 | w := m.Current() | 387 | w := m.Current() | ||
| 372 | - | w.FocusID(7) | 388 | + | w.FocusID(10) |
| 373 | m.ToggleOverview() | 389 | m.ToggleOverview() | ||
| 374 | - | d.move(m, -1, 0) | 390 | + | d.move(m, 0, 1) |
| 375 | if m.cardAt(w) != w.stashAt { | 391 | if m.cardAt(w) != w.stashAt { | ||
| 376 | t.Fatalf("entered at %d", m.cardAt(w)) | 392 | t.Fatalf("entered at %d", m.cardAt(w)) | ||
| 377 | } | 393 | } | ||
| @@ -467,6 +483,23 @@ func TestOverviewCascadeThreeFinger(t *testing.T) { | |||||
| 467 | } | 483 | } | ||
| 468 | } | 484 | } | ||
| 469 | 485 | ||||
| 486 | + | // Three fingers down past the last band enter the stash below it. | |||
| 487 | + | func TestOverviewCascadeThreeFingerDownEntersStash(t *testing.T) { | |||
| 488 | + | c, m := cascadeCore(t) | |||
| 489 | + | withStash(m, 2) | |||
| 490 | + | w := m.Current() | |||
| 491 | + | w.FocusID(6) | |||
| 492 | + | m.ToggleOverview() | |||
| 493 | + | c.swipeBegin(ports.SwipeBegin{Fingers: 3}) | |||
| 494 | + | for range 4 { | |||
| 495 | + | c.swipeUpdate(ports.SwipeUpdate{DY: 40}) | |||
| 496 | + | } | |||
| 497 | + | c.swipeEnd(ports.SwipeEnd{}) | |||
| 498 | + | if m.Current() != w || m.cardAt(w) != w.stashAt { | |||
| 499 | + | t.Fatalf("swipe down from the last band: card %d, want %d", m.cardAt(w), w.stashAt) | |||
| 500 | + | } | |||
| 501 | + | } | |||
| 502 | + | ||||
| 470 | // Right from a stash card lands on the maximized column, which keeps its | 503 | // Right from a stash card lands on the maximized column, which keeps its | ||
| 471 | // maximization: it is alone in its band. | 504 | // maximization: it is alone in its band. | ||
| 472 | func TestOverviewCascadeStashToMaximized(t *testing.T) { | 505 | func TestOverviewCascadeStashToMaximized(t *testing.T) { | ||
| @@ -505,6 +538,61 @@ func TestOverviewCascadeNamedDivider(t *testing.T) { | |||||
| 505 | } | 538 | } | ||
| 506 | } | 539 | } | ||
| 507 | 540 | ||||
| 541 | + | // A stash takes no room in a cascade overview, on a neighbor either: a | |||
| 542 | + | // wide scroll workspace beside it keeps its zoom and slot. | |||
| 543 | + | func TestOverviewCascadeNeighborStashKeepsSlot(t *testing.T) { | |||
| 544 | + | m := cascadeOverview() | |||
| 545 | + | prev := m.Workspaces[0] | |||
| 546 | + | m.Focus(0) | |||
| 547 | + | for id := WindowID(60); id < 64; id++ { | |||
| 548 | + | m.AddWindow(id) | |||
| 549 | + | } | |||
| 550 | + | m.Focus(1) | |||
| 551 | + | m.ToggleOverview() | |||
| 552 | + | bare, _ := m.overviewRows() | |||
| 553 | + | zoom := prev.overviewZoom(horizontalAxis) | |||
| 554 | + | m.CancelOverview() | |||
| 555 | + | if zoom <= overviewMinZoom || zoom >= overviewMaxZoom { | |||
| 556 | + | t.Fatalf("setup: neighbor zoom %v is at a bound", zoom) | |||
| 557 | + | } | |||
| 558 | + | // The same columns, with one more window stashed. | |||
| 559 | + | m.Focus(0) | |||
| 560 | + | m.AddWindow(64) | |||
| 561 | + | prev.FocusID(64) | |||
| 562 | + | prev.ToggleWindowStash() | |||
| 563 | + | m.Focus(1) | |||
| 564 | + | m.ToggleOverview() | |||
| 565 | + | rows, _ := m.overviewRows() | |||
| 566 | + | if rows[prev] != bare[prev] || prev.overviewZoom(horizontalAxis) != zoom { | |||
| 567 | + | t.Fatalf("neighbor slot %v zoom %v with a stash, %v %v without", rows[prev], prev.overviewZoom(horizontalAxis), bare[prev], zoom) | |||
| 568 | + | } | |||
| 569 | + | if p := previewOf(t, m.Layout(), 64); p.Hidden || !p.Peek { | |||
| 570 | + | t.Fatalf("neighbor pile card: %+v", p) | |||
| 571 | + | } | |||
| 572 | + | } | |||
| 573 | + | ||||
| 574 | + | // Down from a covering float enters the stash; right brings the columns | |||
| 575 | + | // back to the front. | |||
| 576 | + | func TestOverviewCascadeFloatStashRight(t *testing.T) { | |||
| 577 | + | m := cascadeOverview() | |||
| 578 | + | withStash(m, 1) | |||
| 579 | + | w := m.Current() | |||
| 580 | + | w.AddFloating(90, 300, 200) | |||
| 581 | + | w.FocusID(90) | |||
| 582 | + | m.ToggleOverview() | |||
| 583 | + | if f := m.stackFront(w); f.kind != stackFloat { | |||
| 584 | + | t.Fatalf("setup: front %+v", f) | |||
| 585 | + | } | |||
| 586 | + | m.OverviewMove(0, 1) | |||
| 587 | + | if m.cardAt(w) < 0 { | |||
| 588 | + | t.Fatal("down from the float did not select the stash") | |||
| 589 | + | } | |||
| 590 | + | m.OverviewMove(1, 0) | |||
| 591 | + | if m.cardAt(w) >= 0 || m.stackFront(w).kind != stackColumns || m.overviewTarget() == 0 { | |||
| 592 | + | t.Fatalf("right from the stash: card %d, front %+v, target %d", m.cardAt(w), m.stackFront(w), m.overviewTarget()) | |||
| 593 | + | } | |||
| 594 | + | } | |||
| 595 | + | ||||
| 508 | // Covering floats fan along the horizontal axis: left goes deeper in the | 596 | // Covering floats fan along the horizontal axis: left goes deeper in the | ||
| 509 | // stack (the cards behind show on the left), right comes back to the front. | 597 | // stack (the cards behind show on the left), right comes back to the front. | ||
| 510 | func TestOverviewCascadeCoveringFloat(t *testing.T) { | 598 | func TestOverviewCascadeCoveringFloat(t *testing.T) { | ||
| @@ -553,8 +641,8 @@ func TestOverviewCascadeCoveringFloat(t *testing.T) { | |||||
| 553 | // The cards behind fan to the left: coming to the front, the | 641 | // The cards behind fan to the left: coming to the front, the | ||
| 554 | // columns move right by one peek step. | 642 | // columns move right by one peek step. | ||
| 555 | front := previewOf(t, m.Layout(), 10) | 643 | front := previewOf(t, m.Layout(), 10) | ||
| 556 | - | if front.Peek || front.Rect.X-behind.Rect.X != w.peekStep() { | 644 | + | if front.Peek || front.Rect.X-behind.Rect.X != w.peekStep(horizontalAxis) { |
| 557 | - | t.Fatalf("columns behind %v, in front %v, peek step %d", behind.Rect, front.Rect, w.peekStep()) | 645 | + | t.Fatalf("columns behind %v, in front %v, peek step %d", behind.Rect, front.Rect, w.peekStep(horizontalAxis)) |
| 558 | } | 646 | } | ||
| 559 | d.move(m, 1, 0) | 647 | d.move(m, 1, 0) | ||
| 560 | if f := m.stackFront(w); f.kind != stackFloat || f.id != 90 { | 648 | if f := m.stackFront(w); f.kind != stackFloat || f.id != 90 { | ||
| @@ -617,9 +705,9 @@ func TestOverviewCascadeFocusBinds(t *testing.T) { | |||||
| 617 | 705 | ||||
| 618 | // A selected stash card keeps the pile cycle. | 706 | // A selected stash card keeps the pile cycle. | ||
| 619 | withStash(m, 3) | 707 | withStash(m, 3) | ||
| 620 | - | w.FocusID(7) | 708 | + | w.FocusID(10) |
| 621 | m.ToggleOverview() | 709 | m.ToggleOverview() | ||
| 622 | - | m.OverviewMove(-1, 0) | 710 | + | m.OverviewMove(0, 1) |
| 623 | at := m.cardAt(w) | 711 | at := m.cardAt(w) | ||
| 624 | m.overviewFocus(ActionFocusWorkspacePrev) | 712 | m.overviewFocus(ActionFocusWorkspacePrev) | ||
| 625 | if m.Current() != w || m.cardAt(w) != at-1 { | 713 | if m.Current() != w || m.cardAt(w) != at-1 { | ||
M internal/core/overview_stack.go +1 -1
| @@ -354,7 +354,7 @@ func (m *Monitor) stackRow(w *Workspace, axis layoutAxis, slot Rect, dim, lit bo | |||
| 354 | 354 | at = slices.Index(items, m.stackFront(w)) | |
| 355 | 355 | } | |
| 356 | 356 | cards := make([][]Placement, len(items)) | |
| 357 | - | step := w.peekStep() | |
| 357 | + | step := w.peekStep(axis) | |
| 358 | 358 | // along is the offset of d steps on the overview axis. | |
| 359 | 359 | along := func(d int) (int, int) { | |
| 360 | 360 | var r Rect | |
| @@ -354,7 +354,7 @@ func (m *Monitor) stackRow(w *Workspace, axis layoutAxis, slot Rect, dim, lit bo | |||||
| 354 | at = slices.Index(items, m.stackFront(w)) | 354 | at = slices.Index(items, m.stackFront(w)) | ||
| 355 | } | 355 | } | ||
| 356 | cards := make([][]Placement, len(items)) | 356 | cards := make([][]Placement, len(items)) | ||
| 357 | - | step := w.peekStep() | 357 | + | step := w.peekStep(axis) |
| 358 | // along is the offset of d steps on the overview axis. | 358 | // along is the offset of d steps on the overview axis. | ||
| 359 | along := func(d int) (int, int) { | 359 | along := func(d int) (int, int) { | ||
| 360 | var r Rect | 360 | var r Rect | ||