perf(overview): lay the stash pile out without per-frame allocations
The folded pile went through fan with one slice per card, and the unfolded strip was built in its own slice then copied: 8 and 1 extra allocations per overview frame. Both now append straight into the overview layout, and fan loses its circular mode, used only by the pile. pileWidth reuses cardSize, and comments describe the pile band under the selected lane.
- commit
- d1a6612e19479dd42ab180bcf6fcb6f6cefeb03b
- author
- brice · · 2026-10-15 05:23 UTC
- parent
- fc79ddf
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +77 -46
M internal/core/overview.go +52 -45
| @@ -60,8 +60,9 @@ type rowSnapshot struct { | |||
| 60 | 60 | // unfolds along the overview axis into a strip of every card (unfold), the | |
| 61 | 61 | // selection framed, and the moves along that axis walk it. A vertical | |
| 62 | 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. | |
| 63 | + | // the pile leaves. A horizontal one centres it on its row, in a band | |
| 64 | + | // reserved under the selected lane (pileBand): the lane and the band are | |
| 65 | + | // centred together. | |
| 65 | 66 | const ( | |
| 66 | 67 | overviewCardZoom = 0.2 | |
| 67 | 68 | overviewCards = 4 | |
| @@ -650,7 +651,7 @@ func (m *Monitor) overviewAxis() layoutAxis { return m.Current().policy().worksp | |||
| 650 | 651 | // | |
| 651 | 652 | // Vertical axis: full-width slots, the current one centred; the rule is | |
| 652 | 653 | // horizontal. Horizontal axis: full-height slots as wide as their content, | |
| 653 | - | // stack peeks included (the stash pile sits over the row), the current one | |
| 654 | + | // stack peeks included (the stash pile takes a band of the slot), the current one | |
| 654 | 655 | // centred; the rule is vertical, in the spacing between two slots. | |
| 655 | 656 | func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | |
| 656 | 657 | cur := m.Current() | |
| @@ -733,7 +734,8 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement { | |||
| 733 | 734 | axis := m.overviewAxis() | |
| 734 | 735 | // The unfolded stash goes last: its strip may run over the neighbor | |
| 735 | 736 | // rows, which must not cover it. | |
| 736 | - | var unfolded []Placement | |
| 737 | + | var unfolded *Workspace | |
| 738 | + | var unfoldedSlot Rect | |
| 737 | 739 | for w := range m.all() { | |
| 738 | 740 | slot, shown := rows[w] | |
| 739 | 741 | if shown { | |
| @@ -742,12 +744,10 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement { | |||
| 742 | 744 | slot.H -= w.pileBand(axis) | |
| 743 | 745 | at := m.cardAt(w) | |
| 744 | 746 | result = append(result, m.stackRow(w, axis, slot, w != cur, at < 0)...) | |
| 745 | - | if len(w.Stash) > 0 { | |
| 746 | - | if cards := w.pile(axis, slot, w != cur, at); at >= 0 { | |
| 747 | - | unfolded = cards | |
| 748 | - | } else { | |
| 749 | - | result = append(result, cards...) | |
| 750 | - | } | |
| 747 | + | if at >= 0 { | |
| 748 | + | unfolded, unfoldedSlot = w, slot | |
| 749 | + | } else { | |
| 750 | + | result = w.pile(result, axis, slot, w != cur, -1) | |
| 751 | 751 | } | |
| 752 | 752 | } | |
| 753 | 753 | for _, f := range w.Floats { | |
| @@ -760,13 +760,16 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement { | |||
| 760 | 760 | result = w.appendHidden(result, false) | |
| 761 | 761 | } | |
| 762 | 762 | } | |
| 763 | - | return append(result, unfolded...) | |
| 763 | + | if unfolded != nil { | |
| 764 | + | result = unfolded.pile(result, axis, unfoldedSlot, false, m.cardAt(unfolded)) | |
| 765 | + | } | |
| 766 | + | return result | |
| 764 | 767 | } | |
| 765 | 768 | ||
| 766 | 769 | // fan places front and neighboring cards, farthest visible first and front | |
| 767 | 770 | // last. A card may contain several tiles; omitted cards stay in the layout | |
| 768 | 771 | // hidden so their clients are not left showing on screen. | |
| 769 | - | func fan(cards [][]Placement, front, maxBehind, maxBefore int, circular bool, | |
| 772 | + | func fan(cards [][]Placement, front, maxBehind, maxBefore int, | |
| 770 | 773 | behind, before func(int) (int, int), dim, lit bool) []Placement { | |
| 771 | 774 | n := len(cards) | |
| 772 | 775 | if n == 0 { | |
| @@ -792,9 +795,6 @@ func fan(cards [][]Placement, front, maxBehind, maxBefore int, circular bool, | |||
| 792 | 795 | // Behind and before are in nearest-first order; paint farthest first. | |
| 793 | 796 | for d := min(maxBehind, n-1); d >= 1; d-- { | |
| 794 | 797 | i := front + d | |
| 795 | - | if circular { | |
| 796 | - | i = (front + d) % n | |
| 797 | - | } | |
| 798 | 798 | if i >= n { | |
| 799 | 799 | continue | |
| 800 | 800 | } | |
| @@ -803,13 +803,7 @@ func fan(cards [][]Placement, front, maxBehind, maxBefore int, circular bool, | |||
| 803 | 803 | } | |
| 804 | 804 | for d := min(maxBefore, n-1); d >= 1; d-- { | |
| 805 | 805 | i := front - d | |
| 806 | - | if circular { | |
| 807 | - | i = (front - d + n) % n | |
| 808 | - | } | |
| 809 | - | if i < 0 { | |
| 810 | - | continue | |
| 811 | - | } | |
| 812 | - | if shown[i] { | |
| 806 | + | if i < 0 || shown[i] { | |
| 813 | 807 | continue | |
| 814 | 808 | } | |
| 815 | 809 | add(i, d, before, true) | |
| @@ -837,7 +831,7 @@ func (w *Workspace) pileWidth() int { | |||
| 837 | 831 | if len(w.Stash) == 0 { | |
| 838 | 832 | return 0 | |
| 839 | 833 | } | |
| 840 | - | card := int(math.Round(float64(w.stashRect().W) * overviewCardZoom)) | |
| 834 | + | card, _ := w.cardSize() | |
| 841 | 835 | // Never more than a third of the width: the columns keep the rest. | |
| 842 | 836 | a := w.overviewArea() | |
| 843 | 837 | return min(card+(min(len(w.Stash), overviewCards)-1)*w.cardStep()+a.W*2/100, a.W/3) | |
| @@ -867,14 +861,15 @@ func (w *Workspace) cardStep() int { | |||
| 867 | 861 | return min(overviewCardStep, a.W/100, a.H/50) | |
| 868 | 862 | } | |
| 869 | 863 | ||
| 870 | - | // pile places the stash of w as cards in its slot along axis (see the | |
| 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 { | |
| 864 | + | // pile appends the stash of w to dst as cards in its slot along axis (see | |
| 865 | + | // the overview's stash comment). With entry at selected (at >= 0, on the | |
| 866 | + | // current row: dim false) the pile unfolds; else it is folded: the stash | |
| 867 | + | // selection in front, painted last, up to overviewCards-1 next ones | |
| 868 | + | // (wrapping round) behind it, farthest first, dimmed; the rest hidden. | |
| 869 | + | func (w *Workspace) pile(dst []Placement, axis layoutAxis, slot Rect, dim bool, at int) []Placement { | |
| 875 | 870 | n := len(w.Stash) | |
| 876 | 871 | if n == 0 { | |
| 877 | - | return nil | |
| 872 | + | return dst | |
| 878 | 873 | } | |
| 879 | 874 | u := w.overviewArea() | |
| 880 | 875 | cw, ch := w.cardSize() | |
| @@ -897,16 +892,28 @@ func (w *Workspace) pile(axis layoutAxis, slot Rect, dim bool, at int) []Placeme | |||
| 897 | 892 | } | |
| 898 | 893 | folded := Rect{X: x0, Y: y0, W: cw, H: ch} | |
| 899 | 894 | if at >= 0 && !dim { | |
| 900 | - | return w.unfold(axis, folded, min(at, n-1)) | |
| 895 | + | return w.unfold(dst, axis, folded, min(at, n-1)) | |
| 896 | + | } | |
| 897 | + | front, behind, step := min(max(w.stashAt, 0), n-1), min(n, overviewCards)-1, w.cardStep() | |
| 898 | + | card := func(i, d int) Placement { | |
| 899 | + | p := Placement{ID: w.Stash[i].ID, Rect: folded, Preview: overviewCardZoom, Card: true} | |
| 900 | + | p.Rect.X, p.Rect.Y = p.Rect.X+d*step, p.Rect.Y-d*step | |
| 901 | + | if d > 0 || dim { | |
| 902 | + | p.peeking(1) | |
| 903 | + | } | |
| 904 | + | return p | |
| 905 | + | } | |
| 906 | + | for d := behind; d >= 0; d-- { | |
| 907 | + | dst = append(dst, card((front+d)%n, d)) | |
| 901 | 908 | } | |
| 902 | - | cards := make([][]Placement, n) | |
| 903 | 909 | for i := range w.Stash { | |
| 904 | - | cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: folded, Preview: overviewCardZoom, Card: true}} | |
| 910 | + | // The cards neither in front nor behind it: hidden, so their | |
| 911 | + | // clients are not left showing. | |
| 912 | + | if (i-front+n)%n > behind { | |
| 913 | + | dst = append(dst, Placement{ID: w.Stash[i].ID, Rect: folded, Card: true, Hidden: true}) | |
| 914 | + | } | |
| 905 | 915 | } | |
| 906 | - | step := w.cardStep() | |
| 907 | - | return fan(cards, min(max(w.stashAt, 0), n-1), min(n, overviewCards)-1, 0, true, | |
| 908 | - | func(d int) (int, int) { return d * step, -d * step }, | |
| 909 | - | func(int) (int, int) { return 0, 0 }, dim, false) | |
| 916 | + | return dst | |
| 910 | 917 | } | |
| 911 | 918 | ||
| 912 | 919 | // unfold lays the stash of w out as a strip of every card along axis, from | |
| @@ -914,8 +921,9 @@ func (w *Workspace) pile(axis layoutAxis, slot Rect, dim bool, at int) []Placeme | |||
| 914 | 921 | // a vertical one. The strip is centred on the pile and kept in the overview | |
| 915 | 922 | // area. Longer than the area, it scrolls like a wide row: the selected card | |
| 916 | 923 | // 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 { | |
| 924 | + | // are dimmed; those out of the area are hidden. The cards are appended to | |
| 925 | + | // dst. | |
| 926 | + | func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int) []Placement { | |
| 919 | 927 | n := len(w.Stash) | |
| 920 | 928 | u := w.overviewArea() | |
| 921 | 929 | size, origin, lo, hi := folded.W, folded.X, u.X, u.X+u.W | |
| @@ -931,7 +939,6 @@ func (w *Workspace) unfold(axis layoutAxis, folded Rect, at int) []Placement { | |||
| 931 | 939 | } else { | |
| 932 | 940 | start = min(max(start, lo), hi-total) | |
| 933 | 941 | } | |
| 934 | - | out := make([]Placement, n) | |
| 935 | 942 | for i := range w.Stash { | |
| 936 | 943 | r := folded | |
| 937 | 944 | if axis == verticalAxis { | |
| @@ -946,9 +953,9 @@ func (w *Workspace) unfold(axis layoutAxis, folded Rect, at int) []Placement { | |||
| 946 | 953 | if !r.Overlaps(u) { | |
| 947 | 954 | p.Hidden, p.Preview, p.Focused = true, 0, false | |
| 948 | 955 | } | |
| 949 | - | out[i] = p | |
| 956 | + | dst = append(dst, p) | |
| 950 | 957 | } | |
| 951 | - | return out | |
| 958 | + | return dst | |
| 952 | 959 | } | |
| 953 | 960 | ||
| 954 | 961 | // overviewZoom is the scale that fits every column of w in the usable | |
| @@ -1138,7 +1145,8 @@ func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes | |||
| 1138 | 1145 | } | |
| 1139 | 1146 | // Rows stay centred on the output like rows without a stash; in a | |
| 1140 | 1147 | // vertical overview the pile only pushes a row right when they would | |
| 1141 | - | // overlap. A horizontal one draws the pile over the row. | |
| 1148 | + | // overlap. A horizontal one gives the pile a band under the selected | |
| 1149 | + | // lane (pileBand). | |
| 1142 | 1150 | left := 0 | |
| 1143 | 1151 | if axis == verticalAxis { | |
| 1144 | 1152 | left = w.pileWidth() | |
| @@ -1312,9 +1320,8 @@ func overviewIn(layout []Placement, x, y float64) WindowID { | |||
| 1312 | 1320 | return 0 | |
| 1313 | 1321 | } | |
| 1314 | 1322 | ||
| 1315 | - | // overviewHit is overviewIn with the stash piles first: a pile is drawn | |
| 1316 | - | // over its row (a horizontal overview), the cards behind its front one | |
| 1317 | - | // included. | |
| 1323 | + | // overviewHit is overviewIn with the stash piles first: the cards behind a | |
| 1324 | + | // folded pile's front one peek out where tiles may be drawn over them. | |
| 1318 | 1325 | func (m *Monitor) overviewHit(layout []Placement, x, y float64) WindowID { | |
| 1319 | 1326 | var piles []Placement | |
| 1320 | 1327 | for _, p := range layout { | |
| @@ -60,8 +60,9 @@ type rowSnapshot struct { | |||||
| 60 | // unfolds along the overview axis into a strip of every card (unfold), the | 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 | 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 | 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 | 63 | + | // the pile leaves. A horizontal one centres it on its row, in a band |
| 64 | - | // bottom edge of the selected lane, over the tiles. | 64 | + | // reserved under the selected lane (pileBand): the lane and the band are |
| 65 | + | // centred together. | |||
| 65 | const ( | 66 | const ( | ||
| 66 | overviewCardZoom = 0.2 | 67 | overviewCardZoom = 0.2 | ||
| 67 | overviewCards = 4 | 68 | overviewCards = 4 | ||
| @@ -650,7 +651,7 @@ func (m *Monitor) overviewAxis() layoutAxis { return m.Current().policy().worksp | |||||
| 650 | // | 651 | // | ||
| 651 | // Vertical axis: full-width slots, the current one centred; the rule is | 652 | // Vertical axis: full-width slots, the current one centred; the rule is | ||
| 652 | // horizontal. Horizontal axis: full-height slots as wide as their content, | 653 | // horizontal. Horizontal axis: full-height slots as wide as their content, | ||
| 653 | - | // stack peeks included (the stash pile sits over the row), the current one | 654 | + | // stack peeks included (the stash pile takes a band of the slot), the current one |
| 654 | // centred; the rule is vertical, in the spacing between two slots. | 655 | // centred; the rule is vertical, in the spacing between two slots. | ||
| 655 | func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | 656 | func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | ||
| 656 | cur := m.Current() | 657 | cur := m.Current() | ||
| @@ -733,7 +734,8 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement { | |||||
| 733 | axis := m.overviewAxis() | 734 | axis := m.overviewAxis() | ||
| 734 | // The unfolded stash goes last: its strip may run over the neighbor | 735 | // The unfolded stash goes last: its strip may run over the neighbor | ||
| 735 | // rows, which must not cover it. | 736 | // rows, which must not cover it. | ||
| 736 | - | var unfolded []Placement | 737 | + | var unfolded *Workspace |
| 738 | + | var unfoldedSlot Rect | |||
| 737 | for w := range m.all() { | 739 | for w := range m.all() { | ||
| 738 | slot, shown := rows[w] | 740 | slot, shown := rows[w] | ||
| 739 | if shown { | 741 | if shown { | ||
| @@ -742,12 +744,10 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement { | |||||
| 742 | slot.H -= w.pileBand(axis) | 744 | slot.H -= w.pileBand(axis) | ||
| 743 | at := m.cardAt(w) | 745 | at := m.cardAt(w) | ||
| 744 | result = append(result, m.stackRow(w, axis, slot, w != cur, at < 0)...) | 746 | result = append(result, m.stackRow(w, axis, slot, w != cur, at < 0)...) | ||
| 745 | - | if len(w.Stash) > 0 { | 747 | + | if at >= 0 { |
| 746 | - | if cards := w.pile(axis, slot, w != cur, at); at >= 0 { | 748 | + | unfolded, unfoldedSlot = w, slot |
| 747 | - | unfolded = cards | 749 | + | } else { |
| 748 | - | } else { | 750 | + | result = w.pile(result, axis, slot, w != cur, -1) |
| 749 | - | result = append(result, cards...) | |||
| 750 | - | } | |||
| 751 | } | 751 | } | ||
| 752 | } | 752 | } | ||
| 753 | for _, f := range w.Floats { | 753 | for _, f := range w.Floats { | ||
| @@ -760,13 +760,16 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement { | |||||
| 760 | result = w.appendHidden(result, false) | 760 | result = w.appendHidden(result, false) | ||
| 761 | } | 761 | } | ||
| 762 | } | 762 | } | ||
| 763 | - | return append(result, unfolded...) | 763 | + | if unfolded != nil { |
| 764 | + | result = unfolded.pile(result, axis, unfoldedSlot, false, m.cardAt(unfolded)) | |||
| 765 | + | } | |||
| 766 | + | return result | |||
| 764 | } | 767 | } | ||
| 765 | 768 | ||||
| 766 | // fan places front and neighboring cards, farthest visible first and front | 769 | // fan places front and neighboring cards, farthest visible first and front | ||
| 767 | // last. A card may contain several tiles; omitted cards stay in the layout | 770 | // last. A card may contain several tiles; omitted cards stay in the layout | ||
| 768 | // hidden so their clients are not left showing on screen. | 771 | // hidden so their clients are not left showing on screen. | ||
| 769 | - | func fan(cards [][]Placement, front, maxBehind, maxBefore int, circular bool, | 772 | + | func fan(cards [][]Placement, front, maxBehind, maxBefore int, |
| 770 | behind, before func(int) (int, int), dim, lit bool) []Placement { | 773 | behind, before func(int) (int, int), dim, lit bool) []Placement { | ||
| 771 | n := len(cards) | 774 | n := len(cards) | ||
| 772 | if n == 0 { | 775 | if n == 0 { | ||
| @@ -792,9 +795,6 @@ func fan(cards [][]Placement, front, maxBehind, maxBefore int, circular bool, | |||||
| 792 | // Behind and before are in nearest-first order; paint farthest first. | 795 | // Behind and before are in nearest-first order; paint farthest first. | ||
| 793 | for d := min(maxBehind, n-1); d >= 1; d-- { | 796 | for d := min(maxBehind, n-1); d >= 1; d-- { | ||
| 794 | i := front + d | 797 | i := front + d | ||
| 795 | - | if circular { | |||
| 796 | - | i = (front + d) % n | |||
| 797 | - | } | |||
| 798 | if i >= n { | 798 | if i >= n { | ||
| 799 | continue | 799 | continue | ||
| 800 | } | 800 | } | ||
| @@ -803,13 +803,7 @@ func fan(cards [][]Placement, front, maxBehind, maxBefore int, circular bool, | |||||
| 803 | } | 803 | } | ||
| 804 | for d := min(maxBefore, n-1); d >= 1; d-- { | 804 | for d := min(maxBefore, n-1); d >= 1; d-- { | ||
| 805 | i := front - d | 805 | i := front - d | ||
| 806 | - | if circular { | 806 | + | if i < 0 || shown[i] { |
| 807 | - | i = (front - d + n) % n | |||
| 808 | - | } | |||
| 809 | - | if i < 0 { | |||
| 810 | - | continue | |||
| 811 | - | } | |||
| 812 | - | if shown[i] { | |||
| 813 | continue | 807 | continue | ||
| 814 | } | 808 | } | ||
| 815 | add(i, d, before, true) | 809 | add(i, d, before, true) | ||
| @@ -837,7 +831,7 @@ func (w *Workspace) pileWidth() int { | |||||
| 837 | if len(w.Stash) == 0 { | 831 | if len(w.Stash) == 0 { | ||
| 838 | return 0 | 832 | return 0 | ||
| 839 | } | 833 | } | ||
| 840 | - | card := int(math.Round(float64(w.stashRect().W) * overviewCardZoom)) | 834 | + | card, _ := w.cardSize() |
| 841 | // Never more than a third of the width: the columns keep the rest. | 835 | // Never more than a third of the width: the columns keep the rest. | ||
| 842 | a := w.overviewArea() | 836 | a := w.overviewArea() | ||
| 843 | return min(card+(min(len(w.Stash), overviewCards)-1)*w.cardStep()+a.W*2/100, a.W/3) | 837 | return min(card+(min(len(w.Stash), overviewCards)-1)*w.cardStep()+a.W*2/100, a.W/3) | ||
| @@ -867,14 +861,15 @@ func (w *Workspace) cardStep() int { | |||||
| 867 | return min(overviewCardStep, a.W/100, a.H/50) | 861 | return min(overviewCardStep, a.W/100, a.H/50) | ||
| 868 | } | 862 | } | ||
| 869 | 863 | ||||
| 870 | - | // pile places the stash of w as cards in its slot along axis (see the | 864 | + | // pile appends the stash of w to dst as cards in its slot along axis (see |
| 871 | - | // overview's stash comment). With entry at selected (at >= 0, on the | 865 | + | // the 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 | 866 | + | // 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. | 867 | + | // selection in front, painted last, up to overviewCards-1 next ones |
| 874 | - | func (w *Workspace) pile(axis layoutAxis, slot Rect, dim bool, at int) []Placement { | 868 | + | // (wrapping round) behind it, farthest first, dimmed; the rest hidden. |
| 869 | + | func (w *Workspace) pile(dst []Placement, axis layoutAxis, slot Rect, dim bool, at int) []Placement { | |||
| 875 | n := len(w.Stash) | 870 | n := len(w.Stash) | ||
| 876 | if n == 0 { | 871 | if n == 0 { | ||
| 877 | - | return nil | 872 | + | return dst |
| 878 | } | 873 | } | ||
| 879 | u := w.overviewArea() | 874 | u := w.overviewArea() | ||
| 880 | cw, ch := w.cardSize() | 875 | cw, ch := w.cardSize() | ||
| @@ -897,16 +892,28 @@ func (w *Workspace) pile(axis layoutAxis, slot Rect, dim bool, at int) []Placeme | |||||
| 897 | } | 892 | } | ||
| 898 | folded := Rect{X: x0, Y: y0, W: cw, H: ch} | 893 | folded := Rect{X: x0, Y: y0, W: cw, H: ch} | ||
| 899 | if at >= 0 && !dim { | 894 | if at >= 0 && !dim { | ||
| 900 | - | return w.unfold(axis, folded, min(at, n-1)) | 895 | + | return w.unfold(dst, axis, folded, min(at, n-1)) |
| 896 | + | } | |||
| 897 | + | front, behind, step := min(max(w.stashAt, 0), n-1), min(n, overviewCards)-1, w.cardStep() | |||
| 898 | + | card := func(i, d int) Placement { | |||
| 899 | + | p := Placement{ID: w.Stash[i].ID, Rect: folded, Preview: overviewCardZoom, Card: true} | |||
| 900 | + | p.Rect.X, p.Rect.Y = p.Rect.X+d*step, p.Rect.Y-d*step | |||
| 901 | + | if d > 0 || dim { | |||
| 902 | + | p.peeking(1) | |||
| 903 | + | } | |||
| 904 | + | return p | |||
| 905 | + | } | |||
| 906 | + | for d := behind; d >= 0; d-- { | |||
| 907 | + | dst = append(dst, card((front+d)%n, d)) | |||
| 901 | } | 908 | } | ||
| 902 | - | cards := make([][]Placement, n) | |||
| 903 | for i := range w.Stash { | 909 | for i := range w.Stash { | ||
| 904 | - | cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: folded, Preview: overviewCardZoom, Card: true}} | 910 | + | // The cards neither in front nor behind it: hidden, so their |
| 911 | + | // clients are not left showing. | |||
| 912 | + | if (i-front+n)%n > behind { | |||
| 913 | + | dst = append(dst, Placement{ID: w.Stash[i].ID, Rect: folded, Card: true, Hidden: true}) | |||
| 914 | + | } | |||
| 905 | } | 915 | } | ||
| 906 | - | step := w.cardStep() | 916 | + | return dst |
| 907 | - | return fan(cards, min(max(w.stashAt, 0), n-1), min(n, overviewCards)-1, 0, true, | |||
| 908 | - | func(d int) (int, int) { return d * step, -d * step }, | |||
| 909 | - | func(int) (int, int) { return 0, 0 }, dim, false) | |||
| 910 | } | 917 | } | ||
| 911 | 918 | ||||
| 912 | // unfold lays the stash of w out as a strip of every card along axis, from | 919 | // unfold lays the stash of w out as a strip of every card along axis, from | ||
| @@ -914,8 +921,9 @@ func (w *Workspace) pile(axis layoutAxis, slot Rect, dim bool, at int) []Placeme | |||||
| 914 | // a vertical one. The strip is centred on the pile and kept in the overview | 921 | // 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 | 922 | // 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 | 923 | // at is centred, the strip stopping at the area's edges. The other cards | ||
| 917 | - | // are dimmed; those out of the area are hidden. | 924 | + | // are dimmed; those out of the area are hidden. The cards are appended to |
| 918 | - | func (w *Workspace) unfold(axis layoutAxis, folded Rect, at int) []Placement { | 925 | + | // dst. |
| 926 | + | func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int) []Placement { | |||
| 919 | n := len(w.Stash) | 927 | n := len(w.Stash) | ||
| 920 | u := w.overviewArea() | 928 | u := w.overviewArea() | ||
| 921 | size, origin, lo, hi := folded.W, folded.X, u.X, u.X+u.W | 929 | size, origin, lo, hi := folded.W, folded.X, u.X, u.X+u.W | ||
| @@ -931,7 +939,6 @@ func (w *Workspace) unfold(axis layoutAxis, folded Rect, at int) []Placement { | |||||
| 931 | } else { | 939 | } else { | ||
| 932 | start = min(max(start, lo), hi-total) | 940 | start = min(max(start, lo), hi-total) | ||
| 933 | } | 941 | } | ||
| 934 | - | out := make([]Placement, n) | |||
| 935 | for i := range w.Stash { | 942 | for i := range w.Stash { | ||
| 936 | r := folded | 943 | r := folded | ||
| 937 | if axis == verticalAxis { | 944 | if axis == verticalAxis { | ||
| @@ -946,9 +953,9 @@ func (w *Workspace) unfold(axis layoutAxis, folded Rect, at int) []Placement { | |||||
| 946 | if !r.Overlaps(u) { | 953 | if !r.Overlaps(u) { | ||
| 947 | p.Hidden, p.Preview, p.Focused = true, 0, false | 954 | p.Hidden, p.Preview, p.Focused = true, 0, false | ||
| 948 | } | 955 | } | ||
| 949 | - | out[i] = p | 956 | + | dst = append(dst, p) |
| 950 | } | 957 | } | ||
| 951 | - | return out | 958 | + | return dst |
| 952 | } | 959 | } | ||
| 953 | 960 | ||||
| 954 | // overviewZoom is the scale that fits every column of w in the usable | 961 | // overviewZoom is the scale that fits every column of w in the usable | ||
| @@ -1138,7 +1145,8 @@ func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes | |||||
| 1138 | } | 1145 | } | ||
| 1139 | // Rows stay centred on the output like rows without a stash; in a | 1146 | // Rows stay centred on the output like rows without a stash; in a | ||
| 1140 | // vertical overview the pile only pushes a row right when they would | 1147 | // vertical overview the pile only pushes a row right when they would | ||
| 1141 | - | // overlap. A horizontal one draws the pile over the row. | 1148 | + | // overlap. A horizontal one gives the pile a band under the selected |
| 1149 | + | // lane (pileBand). | |||
| 1142 | left := 0 | 1150 | left := 0 | ||
| 1143 | if axis == verticalAxis { | 1151 | if axis == verticalAxis { | ||
| 1144 | left = w.pileWidth() | 1152 | left = w.pileWidth() | ||
| @@ -1312,9 +1320,8 @@ func overviewIn(layout []Placement, x, y float64) WindowID { | |||||
| 1312 | return 0 | 1320 | return 0 | ||
| 1313 | } | 1321 | } | ||
| 1314 | 1322 | ||||
| 1315 | - | // overviewHit is overviewIn with the stash piles first: a pile is drawn | 1323 | + | // overviewHit is overviewIn with the stash piles first: the cards behind a |
| 1316 | - | // over its row (a horizontal overview), the cards behind its front one | 1324 | + | // folded pile's front one peek out where tiles may be drawn over them. |
| 1317 | - | // included. | |||
| 1318 | func (m *Monitor) overviewHit(layout []Placement, x, y float64) WindowID { | 1325 | func (m *Monitor) overviewHit(layout []Placement, x, y float64) WindowID { | ||
| 1319 | var piles []Placement | 1326 | var piles []Placement | ||
| 1320 | for _, p := range layout { | 1327 | for _, p := range layout { | ||
M internal/core/overview_stack.go +1 -1
| @@ -366,6 +366,6 @@ func (m *Monitor) stackRow(w *Workspace, axis layoutAxis, slot Rect, dim, lit bo | |||
| 366 | 366 | } | |
| 367 | 367 | ||
| 368 | 368 | // fan's behind side advances through the linear stack. | |
| 369 | - | return fan(cards, at, 2, 2, false, | |
| 369 | + | return fan(cards, at, 2, 2, | |
| 370 | 370 | func(d int) (int, int) { return along(-d) }, along, dim, lit) | |
| 371 | 371 | } | |
| @@ -366,6 +366,6 @@ func (m *Monitor) stackRow(w *Workspace, axis layoutAxis, slot Rect, dim, lit bo | |||||
| 366 | } | 366 | } | ||
| 367 | 367 | ||||
| 368 | // fan's behind side advances through the linear stack. | 368 | // fan's behind side advances through the linear stack. | ||
| 369 | - | return fan(cards, at, 2, 2, false, | 369 | + | return fan(cards, at, 2, 2, |
| 370 | func(d int) (int, int) { return along(-d) }, along, dim, lit) | 370 | func(d int) (int, int) { return along(-d) }, along, dim, lit) | ||
| 371 | } | 371 | } | ||
M internal/core/overview_unfold_test.go +24 -0
| @@ -159,6 +159,30 @@ func TestOverviewStashUnfoldScrolls(t *testing.T) { | |||
| 159 | 159 | } | |
| 160 | 160 | } | |
| 161 | 161 | ||
| 162 | + | // The stash pile, folded or unfolded, is laid out straight into the | |
| 163 | + | // overview layout every frame: it adds no allocation to the same layout | |
| 164 | + | // without a stash. | |
| 165 | + | func TestOverviewStashLayoutAllocations(t *testing.T) { | |
| 166 | + | allocs := func(m *Monitor) float64 { | |
| 167 | + | buf := m.layoutInto(nil) | |
| 168 | + | return testing.AllocsPerRun(50, func() { buf = m.layoutInto(buf) }) | |
| 169 | + | } | |
| 170 | + | bare := cascadeOverview() | |
| 171 | + | bare.ToggleOverview() | |
| 172 | + | want := allocs(bare) | |
| 173 | + | for _, unfolded := range []bool{false, true} { | |
| 174 | + | m := cascadeOverview() | |
| 175 | + | withStash(m, 6) | |
| 176 | + | m.ToggleOverview() | |
| 177 | + | if unfolded { | |
| 178 | + | m.selectCard(m.Current(), 0) | |
| 179 | + | } | |
| 180 | + | if got := allocs(m); got > want { | |
| 181 | + | t.Errorf("unfolded %v: %v allocs per layout, %v without a stash", unfolded, got, want) | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | } | |
| 185 | + | ||
| 162 | 186 | // A scroll overview unfolds its pile into a column on the left of the row; | |
| 163 | 187 | // up and down walk it, right goes back to the columns. | |
| 164 | 188 | func TestOverviewScrollStashUnfolds(t *testing.T) { | |
| @@ -159,6 +159,30 @@ func TestOverviewStashUnfoldScrolls(t *testing.T) { | |||||
| 159 | } | 159 | } | ||
| 160 | } | 160 | } | ||
| 161 | 161 | ||||
| 162 | + | // The stash pile, folded or unfolded, is laid out straight into the | |||
| 163 | + | // overview layout every frame: it adds no allocation to the same layout | |||
| 164 | + | // without a stash. | |||
| 165 | + | func TestOverviewStashLayoutAllocations(t *testing.T) { | |||
| 166 | + | allocs := func(m *Monitor) float64 { | |||
| 167 | + | buf := m.layoutInto(nil) | |||
| 168 | + | return testing.AllocsPerRun(50, func() { buf = m.layoutInto(buf) }) | |||
| 169 | + | } | |||
| 170 | + | bare := cascadeOverview() | |||
| 171 | + | bare.ToggleOverview() | |||
| 172 | + | want := allocs(bare) | |||
| 173 | + | for _, unfolded := range []bool{false, true} { | |||
| 174 | + | m := cascadeOverview() | |||
| 175 | + | withStash(m, 6) | |||
| 176 | + | m.ToggleOverview() | |||
| 177 | + | if unfolded { | |||
| 178 | + | m.selectCard(m.Current(), 0) | |||
| 179 | + | } | |||
| 180 | + | if got := allocs(m); got > want { | |||
| 181 | + | t.Errorf("unfolded %v: %v allocs per layout, %v without a stash", unfolded, got, want) | |||
| 182 | + | } | |||
| 183 | + | } | |||
| 184 | + | } | |||
| 185 | + | ||||
| 162 | // A scroll overview unfolds its pile into a column on the left of the row; | 186 | // 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. | 187 | // up and down walk it, right goes back to the columns. | ||
| 164 | func TestOverviewScrollStashUnfolds(t *testing.T) { | 188 | func TestOverviewScrollStashUnfolds(t *testing.T) { | ||