1package core
2
3import (
4 "fmt"
5 "slices"
6 "testing"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11// cascadeOverview is overviewMonitor with workspace 2 (current) a cascade of
12// seven columns, 4..10, in three bands of three (the last holds 10 alone),
13// the focus on column 8 (band 2, middle). Workspace 1 (columns 1..3) is a
14// scroll workspace on its left, workspace 3 (column 30) on its right.
15func cascadeOverview() *Monitor {
16 m := overviewMonitor()
17 m.SetBorder(2)
18 m.Focus(1)
19 w := m.Current()
20 w.Overflow = OverflowCascade
21 for id := WindowID(5); id <= 10; id++ {
22 w.AddWindow(id)
23 }
24 m.Focus(2)
25 m.AddWindow(30)
26 m.Focus(1)
27 w.FocusID(8)
28 return m
29}
30
31// withStash stashes windows 40..40+n-1 of the current workspace, with the
32// focus back on column 8.
33func withStash(m *Monitor, n int) {
34 w := m.Current()
35 for id := WindowID(40); id < WindowID(40+n); id++ {
36 w.AddWindow(id)
37 w.FocusID(id)
38 w.ToggleWindowStash()
39 }
40 w.FocusID(8)
41}
42
43// cascadeCore is a Core on one output with workspace 0 holding window 1,
44// workspace 1 (current) a cascade of windows 2..6 in bands of two ({2,3}
45// {4,5} {6}), workspace 2 window 30, and window 2 focused.
46func cascadeCore(t *testing.T) (*Core, *Monitor) {
47 t.Helper()
48 c, _ := indicatorCore(t)
49 m := c.cur().mon
50 m.AddWindow(1)
51 m.Focus(1)
52 w := m.Current()
53 w.Overflow = OverflowCascade
54 for id := WindowID(2); id <= 6; id++ {
55 w.AddWindow(id)
56 }
57 m.normalize()
58 m.Focus(2)
59 m.AddWindow(30)
60 m.Focus(1)
61 w.FocusID(2)
62 return c, m
63}
64
65func centreY(r Rect) int { return r.Y + r.H/2 }
66
67func within1(a, b int) bool { return a-b <= 1 && b-a <= 1 }
68
69func hiddenOff(p Placement) bool { return p.Hidden && p.Preview == 0 && !p.Focused }
70
71// The zoom does not depend on the number of bands.
72func TestOverviewCascadeZoomIgnoresBandCount(t *testing.T) {
73 if one, three := cascadeWorkspace(3).overviewZoom(), cascadeWorkspace(7).overviewZoom(); one != three || one != overviewMaxZoom {
74 t.Fatalf("zoom with one band %v, with three %v", one, three)
75 }
76}
77
78// A maximized column keeps its full size in the preview (the workspace's
79// ratio); the columns it hides are dimmed cards at their cell size behind
80// it, peeking out on their side, the nearest one step, the farther ones two.
81// The row widens by that room; other bands keep their cells.
82func TestOverviewCascadeMaximizedColumnCards(t *testing.T) {
83 // Four columns, three per band: band 0 holds 0..2, band 1 holds 3.
84 for _, tc := range []struct {
85 n, max int
86 cards []int // per column: 0 not a card, -k/+k k steps left/right
87 order []int // paint order of band 0's columns
88 }{
89 {n: 1, max: 0, cards: []int{0}, order: []int{0}},
90 {n: 4, max: 0, cards: []int{0, 1, 2, 0}, order: []int{2, 1, 0}},
91 {n: 4, max: 1, cards: []int{-1, 0, 1, 0}, order: []int{0, 2, 1}},
92 {n: 4, max: 3, cards: []int{0, 0, 0, 0}},
93 // Partial bands: the card has its band's half width.
94 {n: 2, max: 0, cards: []int{0, 1}, order: []int{1, 0}},
95 {n: 5, max: 4, cards: []int{0, 0, 0, -1, 0}},
96 } {
97 w := cascadeWorkspace(tc.n)
98 w.Focus = tc.max
99 w.maximize(tc.max)
100 tiles, span, sel := w.previewTiles()
101 g, h, s := w.gap(), w.Usable.H-2*w.gap(), w.bandCardStep()
102 off := 0
103 if slices.ContainsFunc(tc.cards, func(k int) bool { return k != 0 }) {
104 off = 2 * s
105 }
106 if span != w.Usable.W+2*off {
107 t.Fatalf("n=%d max=%d span %d, want %d", tc.n, tc.max, span, w.Usable.W+2*off)
108 }
109 var order []int
110 for _, p := range tiles {
111 i := int(p.ID) - 1
112 if i/3 == 0 && tc.order != nil {
113 order = append(order, i)
114 }
115 band := i / 3
116 want := Rect{X: off + w.cellX(i), Y: g + band*w.Usable.H, W: w.cellWidth(i), H: h}
117 switch k := tc.cards[i]; {
118 case i == tc.max:
119 // Full size, as on screen: the workspace's ratio.
120 want.X, want.W = off+g, w.Usable.W-2*g
121 case k < 0:
122 want.X = g + (2+k)*s
123 case k > 0:
124 want.X = w.Usable.W - g + (2+k)*s - want.W
125 }
126 if p.Rect.X < 0 || p.Rect.X+p.Rect.W > span {
127 t.Fatalf("n=%d max=%d column %d %+v outside span %d", tc.n, tc.max, i, p.Rect, span)
128 }
129 if p.Hidden || p.Peek != (tc.cards[i] != 0) || p.Focused != (i == tc.max) || p.Rect != want {
130 t.Fatalf("n=%d max=%d column %d: %+v, want rect %+v card %v", tc.n, tc.max, i, p, want, tc.cards[i] != 0)
131 }
132 // The 900-wide band split among its columns, no gaps.
133 if inBand := min(tc.n-band*3, 3); i != tc.max && p.Rect.W != 900/inBand {
134 t.Fatalf("n=%d max=%d column %d width %d, want %d", tc.n, tc.max, i, p.Rect.W, 900/inBand)
135 }
136 if i == tc.max && sel != want {
137 t.Fatalf("n=%d max=%d sel %+v, want %+v", tc.n, tc.max, sel, want)
138 }
139 }
140 if tc.order != nil && !slices.Equal(order, tc.order) {
141 t.Fatalf("n=%d max=%d paint order %v, want %v", tc.n, tc.max, order, tc.order)
142 }
143 }
144}
145
146// Moving onto a card behind a maximized column hands it the maximization;
147// Return keeps it there, Escape gives it back. Moving to another band
148// unmaximizes as before.
149func TestOverviewCascadeMaximizedCardSelection(t *testing.T) {
150 for _, d := range cascadeDrivers {
151 t.Run(d.name, func(t *testing.T) {
152 m := cascadeOverview()
153 w := m.Current()
154 w.FocusID(9) // band 2 holds 7, 8, 9
155 w.ToggleFullWidth()
156 m.ToggleOverview()
157 for _, id := range []WindowID{7, 8} {
158 if p := previewOf(t, m.Layout(), id); p.Hidden || !p.Peek {
159 t.Fatalf("card %d: %+v", id, p)
160 }
161 }
162 d.move(m, -1, 0)
163 if focusedID(m) != 8 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(9)].FullWidth {
164 t.Fatalf("left: focus %d, maximization not moved", focusedID(m))
165 }
166 if p := previewOf(t, m.Layout(), 9); !p.Peek {
167 t.Fatalf("former maximized column not a card: %+v", p)
168 }
169 m.CancelOverview()
170 if focusedID(m) != 9 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(8)].FullWidth {
171 t.Fatalf("escape: focus %d, maximization not restored", focusedID(m))
172 }
173
174 m.ToggleOverview()
175 d.move(m, -1, 0)
176 d.move(m, -1, 0)
177 m.ToggleOverview() // return
178 if focusedID(m) != 7 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(9)].FullWidth {
179 t.Fatalf("return: focus %d, maximization not on 7", focusedID(m))
180 }
181
182 m.ToggleOverview()
183 d.move(m, 0, -1)
184 if focusedID(m) != 4 || slices.ContainsFunc(w.Columns, func(c Column) bool { return c.FullWidth }) {
185 t.Fatalf("up: focus %d, a column still maximized", focusedID(m))
186 }
187 })
188 }
189}
190
191// A click on a window of a card's column records that window in the
192// maximize history, not the column's former focus.
193func TestOverviewCascadeMaximizedCardPickRecordsWindow(t *testing.T) {
194 m := cascadeOverview()
195 w := m.Current()
196 // Column 8 stacks 8 over 50, 8 focused.
197 w.Columns[w.columnOf(8)].Windows = append(w.Columns[w.columnOf(8)].Windows, 50)
198 w.FocusID(7)
199 w.ToggleFullWidth()
200 m.ToggleOverview()
201 m.OverviewPick(50)
202 if len(w.maximized) == 0 || w.maximized[0] != 50 || focusedID(m) != 50 {
203 t.Fatalf("history %v, focus %d; want 50 first", w.maximized, focusedID(m))
204 }
205}
206
207// A FullWidth column outside the focused band fills its band, shifted like
208// every tile by the room of the focused column's cards.
209func TestOverviewCascadeUnfocusedMaximizedFillsBand(t *testing.T) {
210 w := cascadeWorkspace(6)
211 w.SetGaps(8)
212 w.Focus = 4
213 w.maximize(4)
214 w.Columns[0].FullWidth = true
215 tiles, _, _ := w.previewTiles()
216 // The row keeps room for the focused column's cards on its left.
217 g, off := w.gap(), 2*w.bandCardStep()
218 p := previewOf(t, tiles, 1)
219 if p.Hidden || p.Rect.X != off+g || p.Rect.W != w.Usable.W-2*g {
220 t.Fatalf("unfocused maximized column %+v, want X %d W %d", p, off+g, w.Usable.W-2*g)
221 }
222}
223
224// A click on a card behind a maximized cascade column hands it the
225// maximization.
226func TestOverviewCascadeMaximizedCardPick(t *testing.T) {
227 m := cascadeOverview()
228 w := m.Current()
229 w.FocusID(7)
230 w.ToggleFullWidth()
231 m.ToggleOverview()
232 // Each card's strip right of the maximized column hits it: 8 next to
233 // it, 9 at the band's edge.
234 ps := m.Layout()
235 front, near, far := previewOf(t, ps, 7).Rect, previewOf(t, ps, 8).Rect, previewOf(t, ps, 9).Rect
236 for _, c := range []struct {
237 x0, x1 int
238 want WindowID
239 }{{front.X + front.W, near.X + near.W, 8}, {near.X + near.W, far.X + far.W, 9}} {
240 x := (c.x0 + c.x1) / 2
241 if c.x0 >= c.x1 {
242 t.Fatalf("no strip for %d: front %v near %v far %v", c.want, front, near, far)
243 }
244 if got := overviewIn(ps, float64(x), float64(centreY(far))); got != c.want {
245 t.Fatalf("strip at x=%d hits %d, want %d", x, got, c.want)
246 }
247 }
248 m.OverviewPick(9)
249 if m.ov.open || focusedID(m) != 9 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(7)].FullWidth {
250 t.Fatalf("pick: open %v, focus %d, maximization not moved", m.ov.open, focusedID(m))
251 }
252}
253
254// The selected band is the centre lane: undimmed, centred, selected tile
255// focused.
256func TestOverviewCascadeCentreLane(t *testing.T) {
257 m := cascadeOverview()
258 m.ToggleOverview()
259 ps := m.Layout()
260 u := m.Current().overviewArea()
261 for _, id := range []WindowID{7, 8, 9} {
262 p := previewOf(t, ps, id)
263 if p.Hidden || p.Peek || p.Preview != overviewMaxZoom || p.Focused != (id == 8) {
264 t.Fatalf("band tile %d: %+v", id, p)
265 }
266 if !within1(centreY(p.Rect), u.Y+u.H/2) {
267 t.Fatalf("band tile %d centre %d, area centre %d", id, centreY(p.Rect), u.Y+u.H/2)
268 }
269 }
270}
271
272// The other bands are dimmed lanes a screen (and a gap) away; those
273// outside the area are hidden.
274func TestOverviewCascadeOtherLanes(t *testing.T) {
275 m := cascadeOverview()
276 m.ToggleOverview()
277 ps := m.Layout()
278 sel := previewOf(t, ps, 8).Rect
279 above, below := previewOf(t, ps, 5), previewOf(t, ps, 10)
280 if above.Hidden || !above.Peek || above.Focused || below.Hidden || !below.Peek || below.Focused {
281 t.Fatalf("adjacent bands: above %+v, below %+v", above, below)
282 }
283 // The lanes sit evenly around the selected one, apart from it (a gap),
284 // and the area still shows part of each.
285 up, down := sel.Y-above.Rect.Y, below.Rect.Y-sel.Y
286 if !within1(up, down) {
287 t.Fatalf("lanes not evenly spaced: %d above, %d below", up, down)
288 }
289 if gap := up - sel.H; gap <= 0 {
290 t.Fatalf("lane above overlaps the selected one: %v over %v", above.Rect, sel)
291 }
292 u := m.Current().overviewArea()
293 if above.Rect.Y+above.Rect.H <= u.Y || below.Rect.Y >= u.Y+u.H {
294 t.Fatalf("adjacent lanes not visible in %v: %v, %v", u, above.Rect, below.Rect)
295 }
296
297 // Five bands (13 columns): the ones two lanes away are off the area.
298 m = cascadeOverview()
299 w := m.Current()
300 for id := WindowID(11); id <= 16; id++ {
301 w.AddWindow(id)
302 }
303 w.FocusID(8)
304 m.ToggleOverview()
305 // Columns 4..16: bands {4,5,6} {7,8,9} {10,11,12} {13,14,15} {16}.
306 w.FocusID(11)
307 ps = m.Layout()
308 for _, id := range []WindowID{4, 5, 6, 16} {
309 if p := previewOf(t, ps, id); !hiddenOff(p) {
310 t.Fatalf("band two lanes away %d not hidden: %+v", id, p)
311 }
312 }
313 for _, id := range []WindowID{7, 13} {
314 if p := previewOf(t, ps, id); p.Hidden || !p.Peek {
315 t.Fatalf("adjacent band %d: %+v", id, p)
316 }
317 }
318}
319
320// Neighbor workspaces sit left and right of the current slot, dimmed,
321// their selected lane level with the centre lane.
322func TestOverviewCascadeNeighbors(t *testing.T) {
323 m := cascadeOverview()
324 m.ToggleOverview()
325 rows, _ := m.overviewRows()
326 cur, left, right := rows[m.Workspaces[1]], rows[m.Workspaces[0]], rows[m.Workspaces[2]]
327 if left.X+left.W > cur.X || right.X < cur.X+cur.W {
328 t.Fatalf("slots overlap: %v %v %v", left, cur, right)
329 }
330 ps := m.Layout()
331 centre := centreY(previewOf(t, ps, 8).Rect)
332 shown := 0
333 for _, n := range []struct {
334 id WindowID
335 side int
336 }{{1, -1}, {2, -1}, {3, -1}, {30, 1}} {
337 id, side := n.id, n.side
338 p := previewOf(t, ps, id)
339 if p.Hidden {
340 continue
341 }
342 shown++
343 if !p.Peek || p.Focused || p.Preview == 0 {
344 t.Fatalf("neighbor %d not dimmed: %+v", id, p)
345 }
346 if side < 0 && p.Rect.X+p.Rect.W > cur.X || side > 0 && p.Rect.X < cur.X+cur.W {
347 t.Fatalf("neighbor %d overlaps the current slot %v: %+v", id, cur, p.Rect)
348 }
349 if !within1(centreY(p.Rect), centre) {
350 t.Fatalf("neighbor %d centre %d, centre lane %d", id, centreY(p.Rect), centre)
351 }
352 }
353 if shown < 2 {
354 t.Fatalf("only %d neighbor tiles drawn", shown)
355 }
356 for _, p := range ps {
357 if p.Hidden || p.Peek {
358 continue
359 }
360 if p.Rect.X < cur.X || p.Rect.X+p.Rect.W > cur.X+cur.W {
361 t.Fatalf("current tile %d outside its slot %v: %+v", p.ID, cur, p.Rect)
362 }
363 }
364}
365
366// Lanes along the overview axis would overlap the other workspaces: a
367// cascade neighbor of a scroll workspace shows its selected band only.
368func TestOverviewCascadeNeighborBelowScrollShowsOneBand(t *testing.T) {
369 m := cascadeOverview()
370 m.Focus(0)
371 m.ToggleOverview()
372 if m.overviewAxis() != verticalAxis {
373 t.Fatal("setup: scroll overview is not vertical")
374 }
375 ps := m.Layout()
376 for _, id := range []WindowID{4, 5, 6} {
377 if p := previewOf(t, ps, id); !hiddenOff(p) {
378 t.Fatalf("band %d of the neighbor drawn: %+v", id, p)
379 }
380 }
381 if p := previewOf(t, ps, 8); p.Hidden {
382 // The neighbor's selected column is 8.
383 t.Fatalf("selected band hidden: %+v", p)
384 }
385 for _, id := range []WindowID{7, 9} {
386 if p := previewOf(t, ps, id); p.Hidden || !p.Peek {
387 t.Fatalf("selected band tile %d: %+v", id, p)
388 }
389 }
390 if p := previewOf(t, ps, 10); !hiddenOff(p) {
391 t.Fatalf("band 3 drawn: %+v", p)
392 }
393}
394
395// The stash pile is inside the current slot, left of the band; the
396// previous workspace stays clear of it.
397func TestOverviewCascadeStashInSlot(t *testing.T) {
398 m := cascadeOverview()
399 withStash(m, 2)
400 m.ToggleOverview()
401 rows, _ := m.overviewRows()
402 cur, prev := rows[m.Current()], rows[m.Workspaces[0]]
403 ps := m.Layout()
404 left := cur.X + cur.W
405 for _, id := range []WindowID{7, 8, 9} {
406 left = min(left, previewOf(t, ps, id).Rect.X)
407 }
408 for _, id := range []WindowID{40, 41} {
409 p := previewOf(t, ps, id)
410 if p.Hidden || p.Preview != overviewCardZoom {
411 t.Fatalf("pile card %d: %+v", id, p)
412 }
413 if p.Rect.X < cur.X || p.Rect.X+p.Rect.W > left {
414 t.Fatalf("pile card %d not inside the slot, left of the band (%d..%d): %+v", id, cur.X, left, p.Rect)
415 }
416 // 41 is the front card of the pile.
417 if id == 41 && !within1(centreY(p.Rect), centreY(previewOf(t, ps, 8).Rect)) {
418 t.Fatalf("pile not level with the selected lane: %+v", p.Rect)
419 }
420 }
421 // The pile does not shift the band: its row stays centred on the area.
422 u := m.Current().overviewArea()
423 lo, hi := previewOf(t, ps, 7).Rect, previewOf(t, ps, 9).Rect
424 if mid := (lo.X + hi.X + hi.W) / 2; !within1(mid, u.X+u.W/2) {
425 t.Fatalf("band centre %d with a stash, area centre %d", mid, u.X+u.W/2)
426 }
427 for _, id := range []WindowID{1, 2, 3} {
428 if p := previewOf(t, ps, id); !p.Hidden && p.Rect.X+p.Rect.W > cur.X {
429 t.Fatalf("previous workspace overlaps the slot with the pile: %+v", p.Rect)
430 }
431 }
432 if prev.X+prev.W > cur.X {
433 t.Fatalf("slots overlap: %v %v", prev, cur)
434 }
435}
436
437type cascadeDriver struct {
438 name string
439 move func(m *Monitor, dx, dy int)
440}
441
442func keyDriver(left, right, up, down string) func(m *Monitor, dx, dy int) {
443 return func(m *Monitor, dx, dy int) {
444 name := map[[2]int]string{{-1, 0}: left, {1, 0}: right, {0, -1}: up, {0, 1}: down}[[2]int{dx, dy}]
445 m.overviewKey(ports.KeyEvent{Keysym: name, Pressed: true})
446 }
447}
448
449var cascadeDrivers = []cascadeDriver{
450 {"move", func(m *Monitor, dx, dy int) { m.OverviewMove(dx, dy) }},
451 {"hjkl", keyDriver("h", "l", "k", "j")},
452 {"arrows", keyDriver("Left", "Right", "Up", "Down")},
453}
454
455func focusedID(m *Monitor) WindowID {
456 id, _ := m.Focused()
457 return id
458}
459
460// ↑/↓ change band and stop at the ends; ←/→ walk the band, then leave it
461// for the stash or the neighbor workspaces; Escape restores everything.
462func TestOverviewCascadeKeys(t *testing.T) {
463 for _, d := range cascadeDrivers {
464 t.Run(d.name, func(t *testing.T) {
465 m := cascadeOverview()
466 w, prev, next := m.Current(), m.Workspaces[0], m.Workspaces[2]
467 w.FocusID(5)
468 m.ToggleOverview()
469 sel := func() WindowID { return w.Columns[w.Focus].Windows[0] }
470 d.move(m, 0, 1)
471 if sel() != 8 {
472 t.Fatalf("down: %d, want 8", sel())
473 }
474 d.move(m, 0, 1)
475 d.move(m, 0, 1)
476 if m.Current() != w || sel() != 10 {
477 t.Fatalf("down at the last band: %d on the current workspace %v", sel(), m.Current() == w)
478 }
479 d.move(m, 0, -1)
480 if sel() != 7 {
481 t.Fatalf("up: %d, want 7", sel())
482 }
483 d.move(m, 0, -1)
484 d.move(m, 0, -1)
485 if m.Current() != w || sel() != 4 {
486 t.Fatalf("up at the first band: %d", sel())
487 }
488 d.move(m, 1, 0)
489 d.move(m, 1, 0)
490 if sel() != 6 || m.Current() != w {
491 t.Fatalf("right in the band: %d", sel())
492 }
493 d.move(m, 1, 0)
494 if m.Current() != next {
495 t.Fatal("right past the last column did not reach the next workspace")
496 }
497 m.CancelOverview()
498 if m.Current() != w || focusedID(m) != 5 {
499 t.Fatalf("cancel left %d", focusedID(m))
500 }
501
502 m.ToggleOverview()
503 d.move(m, -1, 0)
504 if sel() != 4 || m.Current() != w {
505 t.Fatal("left within the band")
506 }
507 d.move(m, -1, 0)
508 if m.Current() != prev {
509 t.Fatal("left from the first column did not reach the previous workspace")
510 }
511 m.CancelOverview()
512
513 // With a stash: left from the first column enters it, right
514 // comes back to the band's first column, left twice goes to
515 // the previous workspace.
516 withStash(m, 2)
517 w.FocusID(7)
518 m.ToggleOverview()
519 d.move(m, -1, 0)
520 if m.cardAt(w) < 0 {
521 t.Fatal("left from the first column did not select the stash")
522 }
523 d.move(m, 1, 0)
524 if m.cardAt(w) >= 0 || sel() != 7 {
525 t.Fatalf("right from the stash: card %d, column %d", m.cardAt(w), sel())
526 }
527 d.move(m, -1, 0)
528 d.move(m, -1, 0)
529 if m.Current() != prev {
530 t.Fatal("left from the stash did not reach the previous workspace")
531 }
532 m.CancelOverview()
533 if m.Current() != w || focusedID(m) != 7 {
534 t.Fatalf("cancel left %d", focusedID(m))
535 }
536 })
537 }
538}
539
540// T7b: with a stash card selected, up/down and the three-finger vertical
541// swipe cycle the pile as in a vertical overview; confirming shows the picked window.
542func TestOverviewCascadeStashPile(t *testing.T) {
543 for _, d := range cascadeDrivers {
544 t.Run(d.name, func(t *testing.T) {
545 m := cascadeOverview()
546 withStash(m, 3)
547 w := m.Current()
548 w.FocusID(7)
549 m.ToggleOverview()
550 d.move(m, -1, 0)
551 if m.cardAt(w) != w.stashAt {
552 t.Fatalf("entered at %d", m.cardAt(w))
553 }
554 at := m.cardAt(w)
555 d.move(m, 0, -1)
556 if m.Current() != w || m.cardAt(w) != at-1 {
557 t.Fatalf("up: card %d, was %d", m.cardAt(w), at)
558 }
559 d.move(m, 0, 1)
560 d.move(m, 0, 1)
561 d.move(m, 0, 1)
562 if m.Current() != w || m.cardAt(w) != len(w.Stash)-1 {
563 t.Fatalf("down: card %d", m.cardAt(w))
564 }
565 d.move(m, 0, -1)
566 picked := w.Stash[m.cardAt(w)].ID
567 m.ToggleOverview()
568 if focusedID(m) != picked || w.stashHidden {
569 t.Fatalf("confirm on %d, want %d (hidden %v)", focusedID(m), picked, w.stashHidden)
570 }
571 })
572 }
573
574 c, _, sc, _ := stashRig(t, true)
575 m := sc.mon
576 w := m.Current()
577 w.Overflow = OverflowCascade
578 m.Focus(1)
579 m.AddWindow(50)
580 m.Focus(0)
581 m.ToggleOverview()
582 if m.cardAt(w) < 0 {
583 t.Fatal("setup: no card selected")
584 }
585 at, before := m.cardAt(w), stashIDs(w)
586 c.swipeBegin(ports.SwipeBegin{Fingers: 3})
587 for range 3 {
588 c.swipeUpdate(ports.SwipeUpdate{DY: -40})
589 }
590 c.swipeEnd(ports.SwipeEnd{})
591 if m.Current() != w || m.cardAt(w) != at-1 {
592 t.Fatalf("swipe up: card %d, was %d", m.cardAt(w), at)
593 }
594 c.swipeBegin(ports.SwipeBegin{Fingers: 3})
595 for range 3 {
596 c.swipeUpdate(ports.SwipeUpdate{DX: 40})
597 }
598 c.swipeEnd(ports.SwipeEnd{})
599 // A horizontal three-finger swipe is a workspace step, even over a
600 // stash card.
601 if m.Current() != m.Workspaces[1] {
602 t.Fatalf("horizontal swipe over a stash card: workspace %d", indexOf(m.Workspaces, m.Current()))
603 }
604 if got := stashIDs(w); !slices.Equal(got, before) {
605 t.Fatal("stash reordered")
606 }
607}
608
609// T7c: three fingers follow the physical axes, once per gesture.
610func TestOverviewCascadeThreeFinger(t *testing.T) {
611 c, m := cascadeCore(t)
612 w := m.Current()
613 m.ToggleOverview()
614 swipe := func(dx, dy float64) {
615 t.Helper()
616 c.swipeBegin(ports.SwipeBegin{Fingers: 3})
617 for range 4 {
618 c.swipeUpdate(ports.SwipeUpdate{DX: dx, DY: dy})
619 }
620 c.swipeEnd(ports.SwipeEnd{})
621 }
622 swipe(0, 40)
623 if m.Current() != w || w.band(w.Focus) != 1 {
624 t.Fatalf("one swipe down: band %d", w.band(w.Focus))
625 }
626 swipe(0, -40)
627 if w.band(w.Focus) != 0 {
628 t.Fatalf("one swipe up: band %d", w.band(w.Focus))
629 }
630 swipe(40, 0)
631 if m.Current() != m.Workspaces[2] {
632 t.Fatalf("one swipe right: workspace %d", indexOf(m.Workspaces, m.Current()))
633 }
634 // Onto a scroll workspace and back: the vertical swipe is the scroll
635 // overview's workspace step; neither bounces during the gesture.
636 swipe(0, -40)
637 if m.Current() != w {
638 t.Fatalf("swipe up from the scroll workspace: workspace %d", indexOf(m.Workspaces, m.Current()))
639 }
640 swipe(-40, 0)
641 if m.Current() != m.Workspaces[0] {
642 t.Fatalf("one swipe left: workspace %d", indexOf(m.Workspaces, m.Current()))
643 }
644}
645
646// Three fingers sideways walk the cards behind a maximized column one per
647// gesture, like the arrows, then change workspace past the band's edge.
648func TestOverviewCascadeThreeFingerMaximizedCards(t *testing.T) {
649 c, m := cascadeCore(t)
650 w := m.Current()
651 w.ToggleFullWidth() // Column 2 hides 3, the other column of its band.
652 m.ToggleOverview()
653 swipe := func(dx float64) {
654 t.Helper()
655 c.swipeBegin(ports.SwipeBegin{Fingers: 3})
656 for range 4 {
657 c.swipeUpdate(ports.SwipeUpdate{DX: dx})
658 }
659 c.swipeEnd(ports.SwipeEnd{})
660 }
661 swipe(40)
662 if m.Current() != w || m.overviewTarget() != 3 || !w.Columns[w.Focus].FullWidth {
663 t.Fatalf("swipe right: workspace %d, target %d, want maximized 3", indexOf(m.Workspaces, m.Current()), m.overviewTarget())
664 }
665 swipe(-40)
666 if m.Current() != w || m.overviewTarget() != 2 || !w.Columns[w.Focus].FullWidth {
667 t.Fatalf("swipe left: workspace %d, target %d, want maximized 2", indexOf(m.Workspaces, m.Current()), m.overviewTarget())
668 }
669 swipe(40)
670 swipe(40)
671 if m.Current() != m.Workspaces[2] {
672 t.Fatalf("swipe right past the band: workspace %d", indexOf(m.Workspaces, m.Current()))
673 }
674
675 // A covering float in front: the swipe changes workspace, as without
676 // a maximized column.
677 m.CancelOverview()
678 w.AddFloating(90, 300, 200)
679 m.ToggleOverview()
680 if f := m.stackFront(w); f.kind != stackFloat || !w.Columns[w.Focus].FullWidth {
681 t.Fatalf("setup: front %+v", f)
682 }
683 swipe(-40)
684 if m.Current() != m.Workspaces[0] {
685 t.Fatalf("swipe left over a float: workspace %d", indexOf(m.Workspaces, m.Current()))
686 }
687}
688
689// Left from the first column of a band whose maximized column hides it:
690// the arrows and three fingers walk the cards, then enter the stash, or
691// change workspace without one.
692func TestOverviewCascadeMaximizedLeftEdge(t *testing.T) {
693 for _, stash := range []bool{false, true} {
694 c, m := cascadeCore(t)
695 w := m.Current()
696 if stash {
697 w.AddWindow(40)
698 w.FocusID(40)
699 w.ToggleWindowStash()
700 }
701 w.FocusID(3) // Band {2, 3}: 3 hides 2.
702 w.ToggleFullWidth()
703 m.ToggleOverview()
704 swipe := func() {
705 c.swipeBegin(ports.SwipeBegin{Fingers: 3})
706 for range 4 {
707 c.swipeUpdate(ports.SwipeUpdate{DX: -40})
708 }
709 c.swipeEnd(ports.SwipeEnd{})
710 }
711 swipe()
712 if m.Current() != w || m.overviewTarget() != 2 || !w.Columns[w.Focus].FullWidth {
713 t.Fatalf("stash %v: first swipe: target %d", stash, m.overviewTarget())
714 }
715 swipe()
716 if stash {
717 if m.Current() != w || m.cardAt(w) < 0 {
718 t.Fatalf("past the band: workspace %d, card %d; want the stash", indexOf(m.Workspaces, m.Current()), m.cardAt(w))
719 }
720 } else if m.Current() != m.Workspaces[0] {
721 t.Fatalf("past the band: workspace %d, want 0", indexOf(m.Workspaces, m.Current()))
722 }
723 }
724}
725
726// With four or more columns in a band, the cards three or more columns
727// away sit under the two-step card: arrows still reach every one, in
728// order, each taking the maximization.
729func TestOverviewCascadeFarCardsByArrows(t *testing.T) {
730 m := newMonitor("", "")
731 m.SetOutput(1200, 600)
732 m.SetMaxColumns(5)
733 w := m.Current()
734 w.Overflow = OverflowCascade
735 for id := WindowID(1); id <= 5; id++ {
736 m.AddWindow(id)
737 }
738 w.FocusID(1)
739 w.ToggleFullWidth()
740 m.ToggleOverview()
741 ps := m.Layout()
742 edge := previewOf(t, ps, 3).Rect
743 for _, id := range []WindowID{4, 5} {
744 if p := previewOf(t, ps, id); p.Hidden || !p.Peek || p.Rect.X != edge.X {
745 t.Fatalf("far card %d: %+v, want a card under 3 at x %d", id, p, edge.X)
746 }
747 }
748 for _, want := range []WindowID{2, 3, 4, 5} {
749 m.OverviewMove(1, 0)
750 if m.Current() != w || m.overviewTarget() != want || !w.Columns[w.Focus].FullWidth {
751 t.Fatalf("right: target %d, want maximized %d", m.overviewTarget(), want)
752 }
753 }
754 m.CancelOverview()
755 if focusedID(m) != 1 || !w.Columns[w.Focus].FullWidth {
756 t.Fatalf("escape: focus %d, want maximized 1", focusedID(m))
757 }
758}
759
760// Right from a stash card lands on the maximized column, which keeps its
761// maximization, not on the first card of its band.
762func TestOverviewCascadeStashToMaximized(t *testing.T) {
763 m := cascadeOverview()
764 withStash(m, 1)
765 w := m.Current()
766 w.FocusID(9)
767 w.ToggleFullWidth()
768 m.ToggleOverview()
769 m.selectCard(w, w.stashAt) // As a click on the pile does.
770 if m.cardAt(w) < 0 {
771 t.Fatalf("setup: no stash card, target %d", m.overviewTarget())
772 }
773 m.OverviewMove(1, 0)
774 if m.overviewTarget() != 9 || !w.Columns[w.Focus].FullWidth || len(w.maximized) == 0 || w.maximized[0] != 9 {
775 t.Fatalf("target %d, maximized %v; want 9", m.overviewTarget(), w.maximized)
776 }
777}
778
779// A named boundary of a cascade overview is a vertical rule strictly
780// between the two slots.
781func TestOverviewCascadeNamedDivider(t *testing.T) {
782 m := namedOverviewMonitor()
783 m.Workspaces[1].Overflow = OverflowCascade
784 m.ToggleOverview()
785 rows, dividers := m.overviewRows()
786 if len(dividers) != 1 {
787 t.Fatalf("dividers: %+v", dividers)
788 }
789 d, cur, dev := dividers[0].Rect, rows[m.Workspaces[1]], rows[m.byName("dev")]
790 if d.W != 1 || d.H <= 1 || dividers[0].Active {
791 t.Fatalf("divider is not an inactive vertical rule: %+v", dividers[0])
792 }
793 if d.X < cur.X+cur.W || d.X+d.W > dev.X {
794 t.Fatalf("divider %v not between %v and %v", d, cur, dev)
795 }
796}
797
798// Covering floats fan along the horizontal axis: left goes deeper in the
799// stack (the cards behind show on the left), right comes back to the front.
800func TestOverviewCascadeCoveringFloat(t *testing.T) {
801 for _, d := range cascadeDrivers {
802 t.Run(d.name, func(t *testing.T) {
803 m := cascadeOverview()
804 m.Current().AddFloating(90, 300, 200)
805 w := m.Current()
806 w.FocusID(10)
807 m.ToggleOverview()
808 if m.stackFront(w).kind != stackColumns {
809 t.Fatal("setup: columns not in front")
810 }
811 d.move(m, 0, -1) // columns: up is a band
812 d.move(m, -1, 0)
813 if f := m.stackFront(w); f.kind != stackFloat || f.id != 90 {
814 t.Fatalf("left from the first column: front %+v", f)
815 }
816 if p := previewOf(t, m.Layout(), 90); p.Peek || !p.Focused {
817 t.Fatalf("float not in front: %+v", p)
818 }
819 d.move(m, 0, 1) // a float front takes no band move
820 if f := m.stackFront(w); f.kind != stackFloat {
821 t.Fatalf("down left the float: %+v", f)
822 }
823 d.move(m, 1, 0)
824 if m.stackFront(w).kind != stackColumns || m.Current() != w {
825 t.Fatalf("right from the float: front %+v", m.stackFront(w))
826 }
827
828 // The float in front of the columns: they fan to its left.
829 m.CancelOverview()
830 w.FocusID(90)
831 m.ToggleOverview()
832 if f := m.stackFront(w); f.kind != stackFloat {
833 t.Fatalf("setup: front %+v", f)
834 }
835 behind := previewOf(t, m.Layout(), 10)
836 if !behind.Peek {
837 t.Fatalf("columns not behind the float: %+v", behind)
838 }
839 d.move(m, -1, 0)
840 if m.stackFront(w).kind != stackColumns || m.Current() != w {
841 t.Fatalf("left from the front float: front %+v", m.stackFront(w))
842 }
843 // The cards behind fan to the left: coming to the front, the
844 // columns move right by one peek step.
845 front := previewOf(t, m.Layout(), 10)
846 if front.Peek || front.Rect.X-behind.Rect.X != w.peekStep() {
847 t.Fatalf("columns behind %v, in front %v, peek step %d", behind.Rect, front.Rect, w.peekStep())
848 }
849 d.move(m, 1, 0)
850 if f := m.stackFront(w); f.kind != stackFloat || f.id != 90 {
851 t.Fatalf("right from the columns behind the float: front %+v", f)
852 }
853 })
854 }
855}
856
857// T10b: a pinned fullscreen column keeps its Fullscreen tile in the band.
858func TestOverviewCascadePinnedFullscreenTile(t *testing.T) {
859 m := cascadeOverview()
860 w := m.Current()
861 w.FocusID(8)
862 m.ToggleFullscreen()
863 if !w.pinned() {
864 t.Fatal("setup: not pinned")
865 }
866 m.ToggleOverview()
867 ps := m.Layout()
868 fs := previewOf(t, ps, 8)
869 if !fs.Fullscreen || fs.Hidden {
870 t.Fatalf("fullscreen tile: %+v", fs)
871 }
872 for _, id := range []WindowID{7, 9} {
873 if p := previewOf(t, ps, id); p.Hidden || p.Fullscreen || p.Peek != (fs.Peek) {
874 t.Fatalf("band tile %d: %+v", id, p)
875 }
876 }
877 u := w.overviewArea()
878 if !within1(centreY(fs.Rect), u.Y+u.H/2) {
879 t.Fatalf("fullscreen tile not on the centre lane: %+v", fs.Rect)
880 }
881}
882
883// The focus binds keep their meaning in a cascade overview.
884func TestOverviewCascadeFocusBinds(t *testing.T) {
885 m := cascadeOverview()
886 w := m.Current()
887 m.ToggleOverview()
888 if !m.overviewFocus(ActionFocusWindowDown) || w.band(w.Focus) != 2 || m.Current() != w {
889 t.Fatalf("focus-window-down: band %d", w.band(w.Focus))
890 }
891 if !m.overviewFocus(ActionFocusWindowUp) || w.band(w.Focus) != 1 {
892 t.Fatalf("focus-window-up: band %d", w.band(w.Focus))
893 }
894 m.overviewFocus(ActionFocusWorkspaceNext)
895 if m.Current() != m.Workspaces[2] {
896 t.Fatal("focus-workspace-next did not change workspace")
897 }
898 m.overviewFocus(ActionFocusWorkspacePrev)
899 if m.Current() != w {
900 t.Fatal("focus-workspace-prev did not come back")
901 }
902 m.overviewFocus(ActionFocusWorkspacePrev)
903 if m.Current() != m.Workspaces[0] {
904 t.Fatal("focus-workspace-prev did not change workspace")
905 }
906 m.CancelOverview()
907
908 // A selected stash card keeps the pile cycle.
909 withStash(m, 3)
910 w.FocusID(7)
911 m.ToggleOverview()
912 m.OverviewMove(-1, 0)
913 at := m.cardAt(w)
914 m.overviewFocus(ActionFocusWorkspacePrev)
915 if m.Current() != w || m.cardAt(w) != at-1 {
916 t.Fatalf("focus-workspace-prev over a card: card %d, was %d", m.cardAt(w), at)
917 }
918 m.overviewFocus(ActionFocusWorkspaceNext)
919 if m.Current() != w || m.cardAt(w) != at {
920 t.Fatalf("focus-workspace-next over a card: card %d", m.cardAt(w))
921 }
922}
923
924// A three-finger step that moves nothing reports no change: no scene
925// transition, no keyboard grab. Here the first band of a cascade has no
926// band above it.
927func TestOverviewCascadeFingerNoopReportsNoChange(t *testing.T) {
928 m := cascadeOverview()
929 m.Current().FocusID(5)
930 m.ToggleOverview()
931 up := ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, Value: -overviewScrollStep}}
932 if m.overviewScroll(up) {
933 t.Fatal("a step past the first band reported a change")
934 }
935}
936
937func TestOverviewMixedLayoutGestureAndCancel(t *testing.T) {
938 m := overviewMonitor()
939 m.SetOverflow(OverflowFixed)
940 m.Current().Overflow = OverflowCascade
941 from := m.Current()
942 m.ToggleOverview()
943 rows, _ := m.overviewRows()
944 if rows[m.Workspaces[1]].X <= rows[from].X {
945 t.Fatal("cascade neighbor not horizontal")
946 }
947 m.overviewScroll(ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: overviewScrollStep}})
948 if m.Current() == from {
949 t.Fatal("horizontal gesture did not change workspace")
950 }
951 if m.Current().Overflow != OverflowFixed {
952 t.Fatal("destination lost fixed policy")
953 }
954 if m.ov.scrollX != 0 || m.ov.scrollY != 0 || !m.ov.scrolled {
955 t.Fatalf("scroll state after the axis change: %+v", m.ov)
956 }
957 m.CancelOverview()
958 if m.Current() != from {
959 t.Fatal("cancel lost source workspace")
960 }
961}
962
963// Showing a workspace with another overview axis drops the scroll leftovers
964// of the old one.
965func TestOverviewAxisChangeResetsScroll(t *testing.T) {
966 m := cascadeOverview()
967 m.ToggleOverview()
968 m.ov.scrollX, m.ov.scrollY, m.ov.sideways, m.ov.scrolled = 20, 20, true, true
969 m.showOverview(m.Workspaces[2])
970 if m.ov.scrollX != 0 || m.ov.scrollY != 0 || m.ov.sideways || !m.ov.scrolled {
971 t.Fatalf("scroll kept or latch dropped: %+v", m.ov)
972 }
973 m.ov.scrollX = 20
974 m.showOverview(m.Workspaces[0])
975 if m.ov.scrollX != 20 {
976 t.Fatal("scroll reset between workspaces of the same axis")
977 }
978}
979
980// A reload that changes the current workspace's axes drops the scroll state;
981// one that does not, keeps it.
982func TestOverviewReloadResetsScrollOnAxisChange(t *testing.T) {
983 c, _ := indicatorCore(t)
984 m := c.cur().mon
985 m.AddWindow(1)
986 m.ToggleOverview()
987 w := m.Current()
988 set := func() { m.ov.scrollX, m.ov.scrollY, m.ov.scrolled, m.ov.sideways = 20, 10, true, true }
989 set()
990 cfg := c.cfg
991 cfg.Layout.Gaps++
992 if err := c.apply(cfg); err != nil {
993 t.Fatal(err)
994 }
995 if m.ov.scrollX != 20 || !m.ov.scrolled {
996 t.Fatalf("unrelated reload reset the scroll: %+v", m.ov)
997 }
998 cfg.Layout.Overflow = string(OverflowCascade)
999 if err := c.apply(cfg); err != nil {
1000 t.Fatal(err)
1001 }
1002 if m.Current().policy().workspace != horizontalAxis {
1003 t.Fatal("setup: not cascade")
1004 }
1005 // The reload keeps the overview open on its selection, and cancelling
1006 // still gives the original focus back.
1007 if id, _ := w.Focused(); !m.ov.open || id != 1 {
1008 t.Fatalf("reload lost the overview selection: open %v, focus %d", m.ov.open, id)
1009 }
1010 // The scroll is dropped, but not the latch that waits for the fingers
1011 // to lift.
1012 if m.ov.scrollX != 0 || m.ov.scrollY != 0 || !m.ov.scrolled || m.ov.sideways {
1013 t.Fatalf("axis change kept the scroll or dropped the latch: %+v", m.ov)
1014 }
1015 set()
1016 cfg.Layout.Overflow = string(OverflowFixed)
1017 if err := c.apply(cfg); err != nil {
1018 t.Fatal(err)
1019 }
1020 if m.ov.scrollX != 0 || !m.ov.scrolled {
1021 t.Fatalf("workspace axis change kept the scroll or dropped the latch: %+v", m.ov)
1022 }
1023 m.CancelOverview()
1024 if id, _ := w.Focused(); id != 1 {
1025 t.Fatalf("cancel left the focus on %d", id)
1026 }
1027}
1028
1029// The finger latch outlives an axis change: a gesture that turned the
1030// overview, then was turned back by a key, does not step again.
1031func TestOverviewFingerLatchSurvivesAxisChange(t *testing.T) {
1032 c, m := cascadeCore(t)
1033 w := m.Current()
1034 m.ToggleOverview()
1035 c.swipeBegin(ports.SwipeBegin{Fingers: 3})
1036 c.swipeUpdate(ports.SwipeUpdate{DX: 70})
1037 if m.Current() != m.Workspaces[2] || m.overviewAxis() != verticalAxis {
1038 t.Fatalf("setup: workspace %d", indexOf(m.Workspaces, m.Current()))
1039 }
1040 // Back to the cascade workspace by a key, with the fingers still down.
1041 m.overviewKey(ports.KeyEvent{Keysym: "k", Pressed: true})
1042 if m.Current() != w || m.overviewAxis() != horizontalAxis {
1043 t.Fatal("setup: the key did not turn the overview back")
1044 }
1045 for _, u := range []ports.SwipeUpdate{{DY: 70}, {DX: 70}, {DY: -70}, {DX: -70}} {
1046 c.swipeUpdate(u)
1047 }
1048 c.swipeEnd(ports.SwipeEnd{})
1049 if m.Current() != w || w.Focus != 0 {
1050 t.Fatalf("the gesture stepped again: workspace %d, column %d", indexOf(m.Workspaces, m.Current()), w.Focus)
1051 }
1052 // The next gesture steps again.
1053 c.swipeBegin(ports.SwipeBegin{Fingers: 3})
1054 c.swipeUpdate(ports.SwipeUpdate{DY: 70})
1055 c.swipeEnd(ports.SwipeEnd{})
1056 if w.band(w.Focus) != 1 {
1057 t.Fatalf("the next gesture did not step: band %d", w.band(w.Focus))
1058 }
1059}
1060
1061// A stash-only cascade workspace has no columns to select: right from its
1062// pile goes straight to the next workspace.
1063func TestOverviewCascadeStashOnlyRight(t *testing.T) {
1064 m := overviewMonitor()
1065 m.Focus(1)
1066 w := m.Current()
1067 w.Overflow = OverflowCascade
1068 w.ToggleWindowStash()
1069 m.Focus(2)
1070 m.AddWindow(30)
1071 m.Focus(1)
1072 m.ToggleOverview()
1073 if len(w.Columns) != 0 || m.cardAt(w) < 0 {
1074 t.Fatalf("setup: %d columns, card %d", len(w.Columns), m.cardAt(w))
1075 }
1076 m.OverviewMove(1, 0)
1077 if m.Current() != m.Workspaces[2] {
1078 t.Fatalf("right from the pile: workspace %d", indexOf(m.Workspaces, m.Current()))
1079 }
1080 m.OverviewMove(0, -1) // the next workspace is a scroll one: up goes back
1081 if m.Current() != w || m.cardAt(w) < 0 {
1082 t.Fatal("setup: back on the stash-only workspace without its card")
1083 }
1084 m.OverviewMove(-1, 0)
1085 if m.Current() != m.Workspaces[0] {
1086 t.Fatalf("left from the pile: workspace %d", indexOf(m.Workspaces, m.Current()))
1087 }
1088}
1089
1090// A cascade neighbor beside a cascade workspace draws its other lanes
1091// dimmed, its selected lane level with the centre lane.
1092func TestOverviewCascadeNeighborLanes(t *testing.T) {
1093 m := cascadeOverview()
1094 prev := m.Workspaces[0]
1095 prev.Overflow = OverflowCascade
1096 for id := WindowID(60); id <= 65; id++ {
1097 prev.AddWindow(id) // bands {1,2,3} {60,61,62} {63,64,65}, 65 selected
1098 }
1099 m.ToggleOverview()
1100 ps := m.Layout()
1101 centre := centreY(previewOf(t, ps, 8).Rect)
1102 sel := previewOf(t, ps, 65)
1103 if sel.Hidden || !sel.Peek || sel.Focused || !within1(centreY(sel.Rect), centre) {
1104 t.Fatalf("selected lane of the neighbor: %+v, centre lane %d", sel, centre)
1105 }
1106 above := previewOf(t, ps, 62)
1107 if above.Hidden || !above.Peek || above.Rect.Y+above.Rect.H > sel.Rect.Y {
1108 t.Fatalf("lane above in the neighbor: %+v, selected %v", above, sel.Rect)
1109 }
1110 if p := previewOf(t, ps, 3); !hiddenOff(p) {
1111 t.Fatalf("lane two away in the neighbor: %+v", p)
1112 }
1113}
1114
1115// A scroll neighbor wider than its slot scrolls inside it: what leaves the
1116// slot is hidden, so it never sits on the current slot.
1117func TestOverviewCascadeWideScrollNeighborIsClipped(t *testing.T) {
1118 m := cascadeOverview()
1119 prev := m.Workspaces[0]
1120 for id := WindowID(60); id <= 67; id++ {
1121 prev.AddWindow(id)
1122 }
1123 m.ToggleOverview()
1124 rows, _ := m.overviewRows()
1125 cur, slot := rows[m.Current()], rows[prev]
1126 hidden, shown := 0, 0
1127 for _, p := range m.Layout() {
1128 if !prev.has(p.ID) {
1129 continue
1130 }
1131 if p.Hidden {
1132 hidden++
1133 continue
1134 }
1135 shown++
1136 if p.Rect.X < slot.X || p.Rect.X+p.Rect.W > slot.X+slot.W || p.Rect.X+p.Rect.W > cur.X {
1137 t.Fatalf("tile %d leaves its slot %v or reaches the current slot %v: %v", p.ID, slot, cur, p.Rect)
1138 }
1139 }
1140 if hidden == 0 || shown == 0 {
1141 t.Fatalf("setup: %d tiles shown, %d hidden", shown, hidden)
1142 }
1143}
1144
1145// A wheel and a continuous scroll in a cascade overview: vertical changes
1146// workspace (and does not bounce back on a workspace with another axis),
1147// horizontal moves along the band.
1148func TestOverviewCascadeScroll(t *testing.T) {
1149 vertical := ports.ScrollAxis{Set: true, Value: overviewScrollStep, V120: 120}
1150 for _, source := range []ports.AxisSource{ports.AxisWheel, ports.AxisContinuous} {
1151 t.Run(fmt.Sprint(source), func(t *testing.T) {
1152 m := cascadeOverview()
1153 w := m.Current()
1154 m.ToggleOverview()
1155 m.overviewScroll(ports.PointerAxis{Source: source, Vertical: vertical})
1156 if m.Current() != m.Workspaces[2] || w.band(w.Focus) != 1 || m.ov.scrollX != 0 || m.ov.scrollY != 0 {
1157 t.Fatalf("vertical: workspace %d, band %d, scroll %v,%v", indexOf(m.Workspaces, m.Current()), w.band(w.Focus), m.ov.scrollX, m.ov.scrollY)
1158 }
1159
1160 m = cascadeOverview()
1161 w = m.Current()
1162 w.FocusID(7)
1163 m.ToggleOverview()
1164 m.overviewScroll(ports.PointerAxis{Source: source, Horizontal: vertical})
1165 if m.Current() != w || w.Columns[w.Focus].Windows[0] != 8 {
1166 t.Fatalf("horizontal: column %d", w.Columns[w.Focus].Windows[0])
1167 }
1168
1169 // Onto a workspace with another axis: one step, no bounce.
1170 m = overviewMonitor()
1171 m.SetOverflow(OverflowFixed)
1172 m.Workspaces[1].Overflow = OverflowCascade
1173 m.ToggleOverview()
1174 m.overviewScroll(ports.PointerAxis{Source: source, Vertical: vertical})
1175 if m.Current() != m.Workspaces[1] || m.ov.scrollX != 0 || m.ov.scrollY != 0 {
1176 t.Fatalf("mixed: workspace %d, scroll %v,%v", indexOf(m.Workspaces, m.Current()), m.ov.scrollX, m.ov.scrollY)
1177 }
1178 })
1179 }
1180}