1package core
2
3import "testing"
4
5// cards are the shown stash placements of w in ps, in stash order.
6func shownCards(t *testing.T, w *Workspace, ps []Placement) []Placement {
7 t.Helper()
8 var out []Placement
9 for _, e := range w.Stash {
10 if p := previewOf(t, ps, e.ID); !p.Hidden {
11 out = append(out, p)
12 }
13 }
14 return out
15}
16
17// Selected, the cascade pile unfolds into a carousel under the lane: every
18// card side by side, none over another, the selection the only framed one,
19// in the middle. Left and right walk it, wrapping round, without leaving
20// the workspace.
21func TestOverviewCascadeStashUnfolds(t *testing.T) {
22 for _, d := range cascadeDrivers {
23 t.Run(d.name, func(t *testing.T) {
24 m := cascadeOverview()
25 withStash(m, 3)
26 w := m.Current()
27 w.FocusID(10)
28 m.ToggleOverview()
29 d.move(m, 0, 1)
30 at := m.cardAt(w)
31 if at < 0 {
32 t.Fatal("setup: no card selected")
33 }
34 ps := m.Layout()
35 cards := shownCards(t, w, ps)
36 if len(cards) != 3 {
37 t.Fatalf("%d cards shown, want 3", len(cards))
38 }
39 lane := previewOf(t, ps, 10).Rect
40 // Left to right: the card before the selection, the
41 // selection, the one after it (wrapping round).
42 row := []Placement{cards[(at+2)%3], cards[at], cards[(at+1)%3]}
43 for i, c := range row {
44 if c.Rect.Y <= lane.Y+lane.H {
45 t.Fatalf("card %d %v not under the lane %v", c.ID, c.Rect, lane)
46 }
47 if i > 0 && (c.Rect.Y != row[0].Rect.Y || c.Rect.X < row[i-1].Rect.X+row[i-1].Rect.W) {
48 t.Fatalf("cards not side by side: %v then %v", row[i-1].Rect, c.Rect)
49 }
50 if c.Focused != (i == 1) {
51 t.Fatalf("card %d focused %v, selection %d", c.ID, c.Focused, at)
52 }
53 }
54 for i := range 4 {
55 d.move(m, -1, 0)
56 if want := (at - i - 1 + 6) % 3; m.Current() != w || m.cardAt(w) != want {
57 t.Fatalf("left %d: card %d, want %d", i, m.cardAt(w), want)
58 }
59 }
60 for i := range 4 {
61 before := m.cardAt(w)
62 d.move(m, 1, 0)
63 if m.Current() != w || m.cardAt(w) != (before+1)%3 {
64 t.Fatalf("right %d: card %d, was %d", i, m.cardAt(w), before)
65 }
66 }
67 })
68 }
69}
70
71// Fewer than three cards keep the stash order without wrapping round: each
72// side has the card its move selects, and past the ends the selection
73// changes workspace, as from a lane.
74func TestOverviewCascadeSmallCarousel(t *testing.T) {
75 for n := 1; n <= 2; n++ {
76 m := cascadeOverview()
77 withStash(m, n)
78 w := m.Current()
79 w.FocusID(10)
80 m.ToggleOverview()
81 m.selectCard(w, 0)
82 if n == 2 {
83 ps := m.Layout()
84 if a, b := previewOf(t, ps, w.Stash[0].ID).Rect.X, previewOf(t, ps, w.Stash[1].ID).Rect.X; b <= a {
85 t.Fatalf("2 cards: the next card at %d, not right of the selection at %d", b, a)
86 }
87 m.OverviewMove(1, 0)
88 if m.Current() != w || m.cardAt(w) != 1 {
89 t.Fatalf("2 cards: right selected card %d", m.cardAt(w))
90 }
91 }
92 m.OverviewMove(1, 0)
93 if m.Current() != m.Workspaces[2] {
94 t.Fatalf("%d cards: right past the last card stayed on workspace %d", n, indexOf(m.Workspaces, m.Current()))
95 }
96 }
97}
98
99// The carousel keeps the stash order, wrapping round: the stride between
100// neighbors is even, wider at the seam between the newest card and the
101// oldest.
102func TestOverviewCascadeCarouselSeam(t *testing.T) {
103 m := cascadeOverview()
104 withStash(m, 5)
105 w := m.Current()
106 m.ToggleOverview()
107 m.selectCard(w, 4)
108 ps := m.Layout()
109 x := func(i int) int { return previewOf(t, ps, w.Stash[i].ID).Rect.X }
110 stride := x(4) - x(3)
111 if stride <= 0 || x(3)-x(2) != stride {
112 t.Fatalf("left of the newest card: %d %d %d", x(2), x(3), x(4))
113 }
114 if seam := x(0) - x(4); seam <= stride {
115 t.Fatalf("seam %d not wider than the stride %d", seam, stride)
116 }
117 if x(1)-x(0) != stride {
118 t.Fatalf("right of the seam: %d %d", x(0), x(1))
119 }
120}
121
122// Down past the last lane enters the stash at its own selection; up leaves
123// it, and the pile folds again.
124func TestOverviewCascadeStashFolds(t *testing.T) {
125 m := cascadeOverview()
126 withStash(m, 3)
127 w := m.Current()
128 w.FocusID(10)
129 m.ToggleOverview()
130 folded := shownCards(t, w, m.Layout())
131 m.OverviewMove(0, 1)
132 m.OverviewMove(0, -1)
133 if m.cardAt(w) >= 0 {
134 t.Fatal("up did not leave the stash")
135 }
136 again := shownCards(t, w, m.Layout())
137 if len(again) != len(folded) {
138 t.Fatalf("folded again: %d cards, was %d", len(again), len(folded))
139 }
140 for i := range again {
141 if again[i].Rect != folded[i].Rect || again[i].Focused {
142 t.Fatalf("card %d folded at %v (focused %v), was %v", again[i].ID, again[i].Rect, again[i].Focused, folded[i].Rect)
143 }
144 }
145}
146
147// A carousel longer than the area keeps every selection centred in it,
148// with cards on both sides, on either axis; the cards past the area are
149// hidden.
150func TestOverviewCarouselScrolls(t *testing.T) {
151 for _, axis := range []layoutAxis{horizontalAxis, verticalAxis} {
152 t.Run(map[layoutAxis]string{horizontalAxis: "cascade", verticalAxis: "scroll"}[axis], func(t *testing.T) {
153 m := cascadeOverview()
154 w := m.Current()
155 if axis == verticalAxis {
156 w.Overflow = OverflowScroll
157 }
158 withStash(m, 12)
159 m.ToggleOverview()
160 if m.overviewAxis() != axis {
161 t.Fatal("setup: wrong overview axis")
162 }
163 m.selectCard(w, 11)
164 u := w.overviewArea()
165 pos := func(r Rect) (int, int) {
166 if axis == verticalAxis {
167 return r.Y, r.Y + r.H/2 - (u.Y + u.H/2)
168 }
169 return r.X, r.X + r.W/2 - (u.X + u.W/2)
170 }
171 for step := range len(w.Stash) + 1 {
172 i := m.cardAt(w)
173 ps := m.Layout()
174 sel := previewOf(t, ps, w.Stash[i].ID)
175 at, off := pos(sel.Rect)
176 if sel.Hidden || !sel.Focused || abs(off) > 1 {
177 t.Fatalf("step %d: card %d not centred: %+v in %v", step, i, sel, u)
178 }
179 before, after, hidden := false, false, false
180 for _, e := range w.Stash {
181 p := previewOf(t, ps, e.ID)
182 if p.Hidden {
183 hidden = true
184 continue
185 }
186 q, _ := pos(p.Rect)
187 before, after = before || q < at, after || q > at
188 }
189 if !before || !after || !hidden {
190 t.Fatalf("step %d: cards before %v, after %v, hidden %v", step, before, after, hidden)
191 }
192 if axis == verticalAxis {
193 m.OverviewMove(0, 1)
194 } else {
195 m.OverviewMove(1, 0)
196 }
197 }
198 })
199 }
200}
201
202// The stash pile, folded or unfolded, is laid out straight into the
203// overview layout every frame: it adds no allocation to the same layout
204// without a stash.
205func TestOverviewStashLayoutAllocations(t *testing.T) {
206 allocs := func(m *Monitor) float64 {
207 buf := m.layoutInto(nil)
208 return testing.AllocsPerRun(50, func() { buf = m.layoutInto(buf) })
209 }
210 bare := cascadeOverview()
211 bare.ToggleOverview()
212 want := allocs(bare)
213 for _, unfolded := range []bool{false, true} {
214 m := cascadeOverview()
215 withStash(m, 6)
216 m.ToggleOverview()
217 if unfolded {
218 m.selectCard(m.Current(), 0)
219 }
220 if got := allocs(m); got > want {
221 t.Errorf("unfolded %v: %v allocs per layout, %v without a stash", unfolded, got, want)
222 }
223 }
224}
225
226// A scroll overview unfolds its pile into a carousel on the left of the
227// row: a column centred on the selection, the card before it above and the
228// one after it below, wrapping round. Up and down walk it round, right goes
229// back to the columns.
230func TestOverviewScrollStashUnfolds(t *testing.T) {
231 m := overviewMonitor()
232 w := m.Current()
233 for id := WindowID(40); id < 43; id++ {
234 w.AddWindow(id)
235 w.FocusID(id)
236 w.ToggleWindowStash()
237 }
238 w.FocusID(1)
239 m.ToggleOverview()
240 if m.overviewAxis() != verticalAxis {
241 t.Fatal("setup: not a vertical overview")
242 }
243 m.OverviewMove(-1, 0)
244 at := m.cardAt(w)
245 if at < 0 {
246 t.Fatal("left from the first column did not select the stash")
247 }
248 cards := shownCards(t, w, m.Layout())
249 if len(cards) != 3 {
250 t.Fatalf("%d cards shown, want 3", len(cards))
251 }
252 col := []Placement{cards[(at+2)%3], cards[at], cards[(at+1)%3]}
253 for i := 1; i < len(col); i++ {
254 if col[i].Rect.X != col[0].Rect.X || col[i].Rect.Y < col[i-1].Rect.Y+col[i-1].Rect.H {
255 t.Fatalf("cards not stacked down: %v then %v", col[i-1].Rect, col[i].Rect)
256 }
257 }
258 if !col[1].Focused {
259 t.Fatal("the selection is not in the middle")
260 }
261 m.OverviewMove(0, 1)
262 if want := (at + 1) % 3; m.cardAt(w) != want {
263 t.Fatalf("down: card %d, want %d", m.cardAt(w), want)
264 }
265 m.OverviewMove(0, -1)
266 m.OverviewMove(0, -1)
267 if want := (at + 2) % 3; m.cardAt(w) != want {
268 t.Fatalf("up twice: card %d, want %d", m.cardAt(w), want)
269 }
270 m.OverviewMove(1, 0)
271 if m.cardAt(w) >= 0 || m.overviewTarget() == 0 {
272 t.Fatalf("right: card %d, target %d", m.cardAt(w), m.overviewTarget())
273 }
274}
275
276// A stash-only workspace's carousel does not wrap round, drawn or walked:
277// the selection stays in the middle, the cards in stash order on either
278// side of it. Past the ends a vertical overview stops; a cascade one
279// changes workspace.
280func TestOverviewStashOnlyCarousel(t *testing.T) {
281 for _, axis := range []layoutAxis{horizontalAxis, verticalAxis} {
282 t.Run(map[layoutAxis]string{horizontalAxis: "cascade", verticalAxis: "scroll"}[axis], func(t *testing.T) {
283 m := overviewMonitor()
284 m.Focus(2)
285 m.AddWindow(30)
286 m.Focus(1)
287 w := m.Current()
288 w.Overflow = OverflowCascade
289 if axis == verticalAxis {
290 w.Overflow = OverflowScroll
291 }
292 w.ToggleWindowStash() // its own window
293 for id := WindowID(40); id < 44; id++ {
294 w.AddWindow(id)
295 w.FocusID(id)
296 w.ToggleWindowStash()
297 }
298 m.ToggleOverview()
299 if m.overviewAxis() != axis || len(w.Columns) != 0 || m.cardAt(w) < 0 {
300 t.Fatalf("setup: axis %d, %d columns, card %d", m.overviewAxis(), len(w.Columns), m.cardAt(w))
301 }
302 last := len(w.Stash) - 1
303 m.selectCard(w, last)
304 ps := m.Layout()
305 pos := func(i int) int {
306 if r := previewOf(t, ps, w.Stash[i].ID).Rect; axis == verticalAxis {
307 return r.Y
308 } else {
309 return r.X
310 }
311 }
312 for i := 1; i <= last; i++ {
313 if pos(i) <= pos(i-1) {
314 t.Fatalf("card %d at %d, not after card %d at %d", i, pos(i), i-1, pos(i-1))
315 }
316 }
317 if axis == verticalAxis {
318 m.OverviewMove(0, 1)
319 if m.Current() != w || m.cardAt(w) != last {
320 t.Fatalf("down past the last card: card %d", m.cardAt(w))
321 }
322 return
323 }
324 m.OverviewMove(1, 0)
325 if m.Current() == w {
326 t.Fatal("right past the last card stayed on the workspace")
327 }
328 })
329 }
330}
331
332// A carousel that does not wrap round and is longer than the area stops at
333// its edges, like a wide row: the first card at the start of the area, the
334// selection inside it.
335func TestOverviewStashOnlyCarouselScrolls(t *testing.T) {
336 m := overviewMonitor()
337 m.Focus(1)
338 w := m.Current()
339 w.Overflow = OverflowCascade
340 w.ToggleWindowStash() // its own window
341 for id := WindowID(40); id < 52; id++ {
342 w.AddWindow(id)
343 w.FocusID(id)
344 w.ToggleWindowStash()
345 }
346 m.ToggleOverview()
347 if w.wrapsCards() || m.cardAt(w) < 0 {
348 t.Fatalf("setup: wraps %v, card %d", w.wrapsCards(), m.cardAt(w))
349 }
350 u := w.overviewArea()
351 m.selectCard(w, 0)
352 ps := m.Layout()
353 if r := previewOf(t, ps, w.Stash[0].ID).Rect; r.X != u.X {
354 t.Fatalf("first card at %d, area starts at %d", r.X, u.X)
355 }
356 m.selectCard(w, len(w.Stash)-1)
357 ps = m.Layout()
358 if r := previewOf(t, ps, w.Stash[len(w.Stash)-1].ID).Rect; r.X+r.W != u.X+u.W {
359 t.Fatalf("last card ends at %d, area at %d", r.X+r.W, u.X+u.W)
360 }
361}
362
363// Stepping to a workspace whose stash has the focus selects its stack when
364// the carousel wraps round, so the next step goes on to the next workspace;
365// a carousel that does not wrap is still selected.
366func TestOverviewStepSkipsWrappingCarousel(t *testing.T) {
367 for _, overflow := range []Overflow{OverflowCascade, OverflowScroll} {
368 for _, n := range []int{2, 3} {
369 m := overviewMonitor()
370 m.Focus(2)
371 m.AddWindow(30)
372 m.Focus(1)
373 w := m.Current()
374 for id := WindowID(40); id < WindowID(40+n); id++ {
375 w.AddWindow(id)
376 w.FocusID(id)
377 w.ToggleWindowStash()
378 }
379 for _, ws := range m.Workspaces[:3] {
380 ws.Overflow = overflow
381 }
382 m.Focus(0)
383 if !w.stashFocused() {
384 t.Fatal("setup: the stash does not have the focus")
385 }
386 m.ToggleOverview()
387 m.overviewFocus(ActionFocusWorkspaceNext)
388 if m.Current() != w {
389 t.Fatalf("%s, %d cards: on workspace %d", overflow, n, indexOf(m.Workspaces, m.Current()))
390 }
391 if got, want := m.cardAt(w) >= 0, n < 3; got != want {
392 t.Fatalf("%s, %d cards: card selected %v, want %v", overflow, n, got, want)
393 }
394 if n < 3 {
395 continue
396 }
397 m.overviewFocus(ActionFocusWorkspaceNext)
398 if m.Current() != m.Workspaces[2] {
399 t.Fatalf("%s: the next step stayed on workspace %d", overflow, indexOf(m.Workspaces, m.Current()))
400 }
401 }
402 }
403}