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// T1: 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 fills its band in the preview as on screen, and hides
79// only the other columns of its band.
80func TestOverviewCascadeMaximizedColumnFillsBand(t *testing.T) {
81 // Four columns, three per band: band 0 holds 0..2, band 1 holds 3.
82 for _, tc := range []struct {
83 n, max int
84 hidden []bool
85 }{
86 {n: 1, max: 0, hidden: []bool{false}},
87 {n: 4, max: 0, hidden: []bool{false, true, true, false}},
88 {n: 4, max: 3, hidden: []bool{false, false, false, false}},
89 } {
90 w := cascadeWorkspace(tc.n)
91 w.Focus = tc.max
92 w.maximize(tc.max)
93 tiles, _, _ := w.previewTiles()
94 g, h := w.gap(), w.Usable.H-2*w.gap()
95 for i, p := range tiles {
96 if p.Hidden != tc.hidden[i] {
97 t.Fatalf("n=%d max=%d column %d hidden %v, want %v", tc.n, tc.max, i, p.Hidden, tc.hidden[i])
98 }
99 if p.Hidden {
100 continue
101 }
102 band := i / 3
103 want := Rect{X: w.cellX(i), Y: g + band*w.Usable.H, W: w.cellWidth(), H: h}
104 if i == tc.max {
105 want.X, want.W = g, w.Usable.W-2*g
106 }
107 if p.Rect != want {
108 t.Fatalf("n=%d max=%d column %d rect %+v, want %+v", tc.n, tc.max, i, p.Rect, want)
109 }
110 }
111 }
112}
113
114// T2: the selected band is the centre lane: undimmed, centred, selected tile
115// focused.
116func TestOverviewCascadeCentreLane(t *testing.T) {
117 m := cascadeOverview()
118 m.ToggleOverview()
119 ps := m.Layout()
120 u := m.Current().overviewArea()
121 for _, id := range []WindowID{7, 8, 9} {
122 p := previewOf(t, ps, id)
123 if p.Hidden || p.Peek || p.Preview != overviewMaxZoom || p.Focused != (id == 8) {
124 t.Fatalf("band tile %d: %+v", id, p)
125 }
126 if !within1(centreY(p.Rect), u.Y+u.H/2) {
127 t.Fatalf("band tile %d centre %d, area centre %d", id, centreY(p.Rect), u.Y+u.H/2)
128 }
129 }
130}
131
132// T3: the other bands are dimmed lanes a screen (and a gap) away; those
133// outside the area are hidden.
134func TestOverviewCascadeOtherLanes(t *testing.T) {
135 m := cascadeOverview()
136 m.ToggleOverview()
137 ps := m.Layout()
138 sel := previewOf(t, ps, 8).Rect
139 above, below := previewOf(t, ps, 5), previewOf(t, ps, 10)
140 if above.Hidden || !above.Peek || above.Focused || below.Hidden || !below.Peek || below.Focused {
141 t.Fatalf("adjacent bands: above %+v, below %+v", above, below)
142 }
143 // The lanes sit evenly around the selected one, apart from it (a gap),
144 // and the area still shows part of each.
145 up, down := sel.Y-above.Rect.Y, below.Rect.Y-sel.Y
146 if !within1(up, down) {
147 t.Fatalf("lanes not evenly spaced: %d above, %d below", up, down)
148 }
149 if gap := up - sel.H; gap <= 0 {
150 t.Fatalf("lane above overlaps the selected one: %v over %v", above.Rect, sel)
151 }
152 u := m.Current().overviewArea()
153 if above.Rect.Y+above.Rect.H <= u.Y || below.Rect.Y >= u.Y+u.H {
154 t.Fatalf("adjacent lanes not visible in %v: %v, %v", u, above.Rect, below.Rect)
155 }
156
157 // Five bands (13 columns): the ones two lanes away are off the area.
158 m = cascadeOverview()
159 w := m.Current()
160 for id := WindowID(11); id <= 16; id++ {
161 w.AddWindow(id)
162 }
163 w.FocusID(8)
164 m.ToggleOverview()
165 // Columns 4..16: bands {4,5,6} {7,8,9} {10,11,12} {13,14,15} {16}.
166 w.FocusID(11)
167 ps = m.Layout()
168 for _, id := range []WindowID{4, 5, 6, 16} {
169 if p := previewOf(t, ps, id); !hiddenOff(p) {
170 t.Fatalf("band two lanes away %d not hidden: %+v", id, p)
171 }
172 }
173 for _, id := range []WindowID{7, 13} {
174 if p := previewOf(t, ps, id); p.Hidden || !p.Peek {
175 t.Fatalf("adjacent band %d: %+v", id, p)
176 }
177 }
178}
179
180// T4: neighbor workspaces sit left and right of the current slot, dimmed,
181// their selected lane level with the centre lane.
182func TestOverviewCascadeNeighbors(t *testing.T) {
183 m := cascadeOverview()
184 m.ToggleOverview()
185 rows, _ := m.overviewRows()
186 cur, left, right := rows[m.Workspaces[1]], rows[m.Workspaces[0]], rows[m.Workspaces[2]]
187 if left.X+left.W > cur.X || right.X < cur.X+cur.W {
188 t.Fatalf("slots overlap: %v %v %v", left, cur, right)
189 }
190 ps := m.Layout()
191 centre := centreY(previewOf(t, ps, 8).Rect)
192 shown := 0
193 for _, n := range []struct {
194 id WindowID
195 side int
196 }{{1, -1}, {2, -1}, {3, -1}, {30, 1}} {
197 id, side := n.id, n.side
198 p := previewOf(t, ps, id)
199 if p.Hidden {
200 continue
201 }
202 shown++
203 if !p.Peek || p.Focused || p.Preview == 0 {
204 t.Fatalf("neighbor %d not dimmed: %+v", id, p)
205 }
206 if side < 0 && p.Rect.X+p.Rect.W > cur.X || side > 0 && p.Rect.X < cur.X+cur.W {
207 t.Fatalf("neighbor %d overlaps the current slot %v: %+v", id, cur, p.Rect)
208 }
209 if !within1(centreY(p.Rect), centre) {
210 t.Fatalf("neighbor %d centre %d, centre lane %d", id, centreY(p.Rect), centre)
211 }
212 }
213 if shown < 2 {
214 t.Fatalf("only %d neighbor tiles drawn", shown)
215 }
216 for _, p := range ps {
217 if p.Hidden || p.Peek {
218 continue
219 }
220 if p.Rect.X < cur.X || p.Rect.X+p.Rect.W > cur.X+cur.W {
221 t.Fatalf("current tile %d outside its slot %v: %+v", p.ID, cur, p.Rect)
222 }
223 }
224}
225
226// T5: lanes along the overview axis would overlap the other workspaces: a
227// cascade neighbor of a scroll workspace shows its selected band only.
228func TestOverviewCascadeNeighborBelowScrollShowsOneBand(t *testing.T) {
229 m := cascadeOverview()
230 m.Focus(0)
231 m.ToggleOverview()
232 if m.overviewAxis() != verticalAxis {
233 t.Fatal("setup: scroll overview is not vertical")
234 }
235 ps := m.Layout()
236 for _, id := range []WindowID{4, 5, 6} {
237 if p := previewOf(t, ps, id); !hiddenOff(p) {
238 t.Fatalf("band %d of the neighbor drawn: %+v", id, p)
239 }
240 }
241 if p := previewOf(t, ps, 8); p.Hidden {
242 // The neighbor's selected column is 8.
243 t.Fatalf("selected band hidden: %+v", p)
244 }
245 for _, id := range []WindowID{7, 9} {
246 if p := previewOf(t, ps, id); p.Hidden || !p.Peek {
247 t.Fatalf("selected band tile %d: %+v", id, p)
248 }
249 }
250 if p := previewOf(t, ps, 10); !hiddenOff(p) {
251 t.Fatalf("band 3 drawn: %+v", p)
252 }
253}
254
255// T6: the stash pile is inside the current slot, left of the band; the
256// previous workspace stays clear of it.
257func TestOverviewCascadeStashInSlot(t *testing.T) {
258 m := cascadeOverview()
259 withStash(m, 2)
260 m.ToggleOverview()
261 rows, _ := m.overviewRows()
262 cur, prev := rows[m.Current()], rows[m.Workspaces[0]]
263 ps := m.Layout()
264 left := cur.X + cur.W
265 for _, id := range []WindowID{7, 8, 9} {
266 left = min(left, previewOf(t, ps, id).Rect.X)
267 }
268 for _, id := range []WindowID{40, 41} {
269 p := previewOf(t, ps, id)
270 if p.Hidden || p.Preview != overviewCardZoom {
271 t.Fatalf("pile card %d: %+v", id, p)
272 }
273 if p.Rect.X < cur.X || p.Rect.X+p.Rect.W > left {
274 t.Fatalf("pile card %d not inside the slot, left of the band (%d..%d): %+v", id, cur.X, left, p.Rect)
275 }
276 // 41 is the front card of the pile.
277 if id == 41 && !within1(centreY(p.Rect), centreY(previewOf(t, ps, 8).Rect)) {
278 t.Fatalf("pile not level with the selected lane: %+v", p.Rect)
279 }
280 }
281 // The pile does not shift the band: its row stays centred on the area.
282 u := m.Current().overviewArea()
283 lo, hi := previewOf(t, ps, 7).Rect, previewOf(t, ps, 9).Rect
284 if mid := (lo.X + hi.X + hi.W) / 2; !within1(mid, u.X+u.W/2) {
285 t.Fatalf("band centre %d with a stash, area centre %d", mid, u.X+u.W/2)
286 }
287 for _, id := range []WindowID{1, 2, 3} {
288 if p := previewOf(t, ps, id); !p.Hidden && p.Rect.X+p.Rect.W > cur.X {
289 t.Fatalf("previous workspace overlaps the slot with the pile: %+v", p.Rect)
290 }
291 }
292 if prev.X+prev.W > cur.X {
293 t.Fatalf("slots overlap: %v %v", prev, cur)
294 }
295}
296
297type cascadeDriver struct {
298 name string
299 move func(m *Monitor, dx, dy int)
300}
301
302func keyDriver(left, right, up, down string) func(m *Monitor, dx, dy int) {
303 return func(m *Monitor, dx, dy int) {
304 name := map[[2]int]string{{-1, 0}: left, {1, 0}: right, {0, -1}: up, {0, 1}: down}[[2]int{dx, dy}]
305 m.overviewKey(ports.KeyEvent{Keysym: name, Pressed: true})
306 }
307}
308
309var cascadeDrivers = []cascadeDriver{
310 {"move", func(m *Monitor, dx, dy int) { m.OverviewMove(dx, dy) }},
311 {"hjkl", keyDriver("h", "l", "k", "j")},
312 {"arrows", keyDriver("Left", "Right", "Up", "Down")},
313}
314
315func focusedID(m *Monitor) WindowID {
316 id, _ := m.Focused()
317 return id
318}
319
320// T7: ↑/↓ change band and stop at the ends; ←/→ walk the band, then leave it
321// for the stash or the neighbor workspaces; Escape restores everything.
322func TestOverviewCascadeKeys(t *testing.T) {
323 for _, d := range cascadeDrivers {
324 t.Run(d.name, func(t *testing.T) {
325 m := cascadeOverview()
326 w, prev, next := m.Current(), m.Workspaces[0], m.Workspaces[2]
327 w.FocusID(5)
328 m.ToggleOverview()
329 sel := func() WindowID { return w.Columns[w.Focus].Windows[0] }
330 d.move(m, 0, 1)
331 if sel() != 8 {
332 t.Fatalf("down: %d, want 8", sel())
333 }
334 d.move(m, 0, 1)
335 d.move(m, 0, 1)
336 if m.Current() != w || sel() != 10 {
337 t.Fatalf("down at the last band: %d on the current workspace %v", sel(), m.Current() == w)
338 }
339 d.move(m, 0, -1)
340 if sel() != 7 {
341 t.Fatalf("up: %d, want 7", sel())
342 }
343 d.move(m, 0, -1)
344 d.move(m, 0, -1)
345 if m.Current() != w || sel() != 4 {
346 t.Fatalf("up at the first band: %d", sel())
347 }
348 d.move(m, 1, 0)
349 d.move(m, 1, 0)
350 if sel() != 6 || m.Current() != w {
351 t.Fatalf("right in the band: %d", sel())
352 }
353 d.move(m, 1, 0)
354 if m.Current() != next {
355 t.Fatal("right past the last column did not reach the next workspace")
356 }
357 m.CancelOverview()
358 if m.Current() != w || focusedID(m) != 5 {
359 t.Fatalf("cancel left %d", focusedID(m))
360 }
361
362 m.ToggleOverview()
363 d.move(m, -1, 0)
364 if sel() != 4 || m.Current() != w {
365 t.Fatal("left within the band")
366 }
367 d.move(m, -1, 0)
368 if m.Current() != prev {
369 t.Fatal("left from the first column did not reach the previous workspace")
370 }
371 m.CancelOverview()
372
373 // With a stash: left from the first column enters it, right
374 // comes back to the band's first column, left twice goes to
375 // the previous workspace.
376 withStash(m, 2)
377 w.FocusID(7)
378 m.ToggleOverview()
379 d.move(m, -1, 0)
380 if m.cardAt(w) < 0 {
381 t.Fatal("left from the first column did not select the stash")
382 }
383 d.move(m, 1, 0)
384 if m.cardAt(w) >= 0 || sel() != 7 {
385 t.Fatalf("right from the stash: card %d, column %d", m.cardAt(w), sel())
386 }
387 d.move(m, -1, 0)
388 d.move(m, -1, 0)
389 if m.Current() != prev {
390 t.Fatal("left from the stash did not reach the previous workspace")
391 }
392 m.CancelOverview()
393 if m.Current() != w || focusedID(m) != 7 {
394 t.Fatalf("cancel left %d", focusedID(m))
395 }
396 })
397 }
398}
399
400// T7b: with a stash card selected, up/down and the three-finger vertical
401// swipe cycle the pile as on main; confirming shows the picked window.
402func TestOverviewCascadeStashPile(t *testing.T) {
403 for _, d := range cascadeDrivers {
404 t.Run(d.name, func(t *testing.T) {
405 m := cascadeOverview()
406 withStash(m, 3)
407 w := m.Current()
408 w.FocusID(7)
409 m.ToggleOverview()
410 d.move(m, -1, 0)
411 if m.cardAt(w) != w.stashAt {
412 t.Fatalf("entered at %d", m.cardAt(w))
413 }
414 at := m.cardAt(w)
415 d.move(m, 0, -1)
416 if m.Current() != w || m.cardAt(w) != at-1 {
417 t.Fatalf("up: card %d, was %d", m.cardAt(w), at)
418 }
419 d.move(m, 0, 1)
420 d.move(m, 0, 1)
421 d.move(m, 0, 1)
422 if m.Current() != w || m.cardAt(w) != len(w.Stash)-1 {
423 t.Fatalf("down: card %d", m.cardAt(w))
424 }
425 d.move(m, 0, -1)
426 picked := w.Stash[m.cardAt(w)].ID
427 m.ToggleOverview()
428 if focusedID(m) != picked || w.stashHidden {
429 t.Fatalf("confirm on %d, want %d (hidden %v)", focusedID(m), picked, w.stashHidden)
430 }
431 })
432 }
433
434 c, _, sc, _ := stashRig(t, true)
435 m := sc.mon
436 w := m.Current()
437 w.Overflow = OverflowCascade
438 m.Focus(1)
439 m.AddWindow(50)
440 m.Focus(0)
441 m.ToggleOverview()
442 if m.cardAt(w) < 0 {
443 t.Fatal("setup: no card selected")
444 }
445 at, before := m.cardAt(w), stashIDs(w)
446 c.swipeBegin(ports.SwipeBegin{Fingers: 3})
447 for range 3 {
448 c.swipeUpdate(ports.SwipeUpdate{DY: -40})
449 }
450 c.swipeEnd(ports.SwipeEnd{})
451 if m.Current() != w || m.cardAt(w) != at-1 {
452 t.Fatalf("swipe up: card %d, was %d", m.cardAt(w), at)
453 }
454 c.swipeBegin(ports.SwipeBegin{Fingers: 3})
455 for range 3 {
456 c.swipeUpdate(ports.SwipeUpdate{DX: 40})
457 }
458 c.swipeEnd(ports.SwipeEnd{})
459 // A horizontal three-finger swipe is a workspace step, even over a
460 // stash card.
461 if m.Current() != m.Workspaces[1] {
462 t.Fatalf("horizontal swipe over a stash card: workspace %d", indexOf(m.Workspaces, m.Current()))
463 }
464 if got := stashIDs(w); !slices.Equal(got, before) {
465 t.Fatal("stash reordered")
466 }
467}
468
469// T7c: three fingers follow the physical axes, once per gesture.
470func TestOverviewCascadeThreeFinger(t *testing.T) {
471 c, m := cascadeCore(t)
472 w := m.Current()
473 m.ToggleOverview()
474 swipe := func(dx, dy float64) {
475 t.Helper()
476 c.swipeBegin(ports.SwipeBegin{Fingers: 3})
477 for range 4 {
478 c.swipeUpdate(ports.SwipeUpdate{DX: dx, DY: dy})
479 }
480 c.swipeEnd(ports.SwipeEnd{})
481 }
482 swipe(0, 40)
483 if m.Current() != w || w.band(w.Focus) != 1 {
484 t.Fatalf("one swipe down: band %d", w.band(w.Focus))
485 }
486 swipe(0, -40)
487 if w.band(w.Focus) != 0 {
488 t.Fatalf("one swipe up: band %d", w.band(w.Focus))
489 }
490 swipe(40, 0)
491 if m.Current() != m.Workspaces[2] {
492 t.Fatalf("one swipe right: workspace %d", indexOf(m.Workspaces, m.Current()))
493 }
494 // Onto a scroll workspace and back: the vertical swipe is the scroll
495 // overview's workspace step; neither bounces during the gesture.
496 swipe(0, -40)
497 if m.Current() != w {
498 t.Fatalf("swipe up from the scroll workspace: workspace %d", indexOf(m.Workspaces, m.Current()))
499 }
500 swipe(-40, 0)
501 if m.Current() != m.Workspaces[0] {
502 t.Fatalf("one swipe left: workspace %d", indexOf(m.Workspaces, m.Current()))
503 }
504}
505
506// T8: a named boundary of a cascade overview is a vertical rule strictly
507// between the two slots.
508func TestOverviewCascadeNamedDivider(t *testing.T) {
509 m := namedOverviewMonitor()
510 m.Workspaces[1].Overflow = OverflowCascade
511 m.ToggleOverview()
512 rows, dividers := m.overviewRows()
513 if len(dividers) != 1 {
514 t.Fatalf("dividers: %+v", dividers)
515 }
516 d, cur, dev := dividers[0].Rect, rows[m.Workspaces[1]], rows[m.byName("dev")]
517 if d.W != 1 || d.H <= 1 || dividers[0].Active {
518 t.Fatalf("divider is not an inactive vertical rule: %+v", dividers[0])
519 }
520 if d.X < cur.X+cur.W || d.X+d.W > dev.X {
521 t.Fatalf("divider %v not between %v and %v", d, cur, dev)
522 }
523}
524
525// T10: covering floats fan along the horizontal axis: left goes deeper in the
526// stack (the cards behind show on the left), right comes back to the front.
527func TestOverviewCascadeCoveringFloat(t *testing.T) {
528 for _, d := range cascadeDrivers {
529 t.Run(d.name, func(t *testing.T) {
530 m := cascadeOverview()
531 m.Current().AddFloating(90, 300, 200)
532 w := m.Current()
533 w.FocusID(10)
534 m.ToggleOverview()
535 if m.stackFront(w).kind != stackColumns {
536 t.Fatal("setup: columns not in front")
537 }
538 d.move(m, 0, -1) // columns: up is a band
539 d.move(m, -1, 0)
540 if f := m.stackFront(w); f.kind != stackFloat || f.id != 90 {
541 t.Fatalf("left from the first column: front %+v", f)
542 }
543 if p := previewOf(t, m.Layout(), 90); p.Peek || !p.Focused {
544 t.Fatalf("float not in front: %+v", p)
545 }
546 d.move(m, 0, 1) // a float front takes no band move
547 if f := m.stackFront(w); f.kind != stackFloat {
548 t.Fatalf("down left the float: %+v", f)
549 }
550 d.move(m, 1, 0)
551 if m.stackFront(w).kind != stackColumns || m.Current() != w {
552 t.Fatalf("right from the float: front %+v", m.stackFront(w))
553 }
554
555 // The float in front of the columns: they fan to its left.
556 m.CancelOverview()
557 w.FocusID(90)
558 m.ToggleOverview()
559 if f := m.stackFront(w); f.kind != stackFloat {
560 t.Fatalf("setup: front %+v", f)
561 }
562 behind := previewOf(t, m.Layout(), 10)
563 if !behind.Peek {
564 t.Fatalf("columns not behind the float: %+v", behind)
565 }
566 d.move(m, -1, 0)
567 if m.stackFront(w).kind != stackColumns || m.Current() != w {
568 t.Fatalf("left from the front float: front %+v", m.stackFront(w))
569 }
570 // The cards behind fan to the left: coming to the front, the
571 // columns move right by one peek step.
572 front := previewOf(t, m.Layout(), 10)
573 if front.Peek || front.Rect.X-behind.Rect.X != w.peekStep() {
574 t.Fatalf("columns behind %v, in front %v, peek step %d", behind.Rect, front.Rect, w.peekStep())
575 }
576 d.move(m, 1, 0)
577 if f := m.stackFront(w); f.kind != stackFloat || f.id != 90 {
578 t.Fatalf("right from the columns behind the float: front %+v", f)
579 }
580 })
581 }
582}
583
584// T10b: a pinned fullscreen column keeps its Fullscreen tile in the band.
585func TestOverviewCascadePinnedFullscreenTile(t *testing.T) {
586 m := cascadeOverview()
587 w := m.Current()
588 w.FocusID(8)
589 m.ToggleFullscreen()
590 if !w.pinned() {
591 t.Fatal("setup: not pinned")
592 }
593 m.ToggleOverview()
594 ps := m.Layout()
595 fs := previewOf(t, ps, 8)
596 if !fs.Fullscreen || fs.Hidden {
597 t.Fatalf("fullscreen tile: %+v", fs)
598 }
599 for _, id := range []WindowID{7, 9} {
600 if p := previewOf(t, ps, id); p.Hidden || p.Fullscreen || p.Peek != (fs.Peek) {
601 t.Fatalf("band tile %d: %+v", id, p)
602 }
603 }
604 u := w.overviewArea()
605 if !within1(centreY(fs.Rect), u.Y+u.H/2) {
606 t.Fatalf("fullscreen tile not on the centre lane: %+v", fs.Rect)
607 }
608}
609
610// T11: the focus binds keep their meaning in a cascade overview.
611func TestOverviewCascadeFocusBinds(t *testing.T) {
612 m := cascadeOverview()
613 w := m.Current()
614 m.ToggleOverview()
615 if !m.overviewFocus(ActionFocusWindowDown) || w.band(w.Focus) != 2 || m.Current() != w {
616 t.Fatalf("focus-window-down: band %d", w.band(w.Focus))
617 }
618 if !m.overviewFocus(ActionFocusWindowUp) || w.band(w.Focus) != 1 {
619 t.Fatalf("focus-window-up: band %d", w.band(w.Focus))
620 }
621 m.overviewFocus(ActionFocusWorkspaceNext)
622 if m.Current() != m.Workspaces[2] {
623 t.Fatal("focus-workspace-next did not change workspace")
624 }
625 m.overviewFocus(ActionFocusWorkspacePrev)
626 if m.Current() != w {
627 t.Fatal("focus-workspace-prev did not come back")
628 }
629 m.overviewFocus(ActionFocusWorkspacePrev)
630 if m.Current() != m.Workspaces[0] {
631 t.Fatal("focus-workspace-prev did not change workspace")
632 }
633 m.CancelOverview()
634
635 // A selected stash card keeps the pile cycle.
636 withStash(m, 3)
637 w.FocusID(7)
638 m.ToggleOverview()
639 m.OverviewMove(-1, 0)
640 at := m.cardAt(w)
641 m.overviewFocus(ActionFocusWorkspacePrev)
642 if m.Current() != w || m.cardAt(w) != at-1 {
643 t.Fatalf("focus-workspace-prev over a card: card %d, was %d", m.cardAt(w), at)
644 }
645 m.overviewFocus(ActionFocusWorkspaceNext)
646 if m.Current() != w || m.cardAt(w) != at {
647 t.Fatalf("focus-workspace-next over a card: card %d", m.cardAt(w))
648 }
649}
650
651func TestOverviewMixedLayoutGestureAndCancel(t *testing.T) {
652 m := overviewMonitor()
653 m.SetOverflow(OverflowFixed)
654 m.Current().Overflow = OverflowCascade
655 from := m.Current()
656 m.ToggleOverview()
657 rows, _ := m.overviewRows()
658 if rows[m.Workspaces[1]].X <= rows[from].X {
659 t.Fatal("cascade neighbor not horizontal")
660 }
661 m.overviewScroll(ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: overviewScrollStep}})
662 if m.Current() == from {
663 t.Fatal("horizontal gesture did not change workspace")
664 }
665 if m.Current().Overflow != OverflowFixed {
666 t.Fatal("destination lost fixed policy")
667 }
668 if m.ov.scrollX != 0 || m.ov.scrollY != 0 || !m.ov.scrolled {
669 t.Fatalf("scroll state after the axis change: %+v", m.ov)
670 }
671 m.CancelOverview()
672 if m.Current() != from {
673 t.Fatal("cancel lost source workspace")
674 }
675}
676
677// Showing a workspace with another overview axis drops the scroll leftovers
678// of the old one.
679func TestOverviewAxisChangeResetsScroll(t *testing.T) {
680 m := cascadeOverview()
681 m.ToggleOverview()
682 m.ov.scrollX, m.ov.scrollY, m.ov.sideways, m.ov.scrolled = 20, 20, true, true
683 m.showOverview(m.Workspaces[2])
684 if m.ov.scrollX != 0 || m.ov.scrollY != 0 || m.ov.sideways || !m.ov.scrolled {
685 t.Fatalf("scroll kept or latch dropped: %+v", m.ov)
686 }
687 m.ov.scrollX = 20
688 m.showOverview(m.Workspaces[0])
689 if m.ov.scrollX != 20 {
690 t.Fatal("scroll reset between workspaces of the same axis")
691 }
692}
693
694// A reload that changes the current workspace's axes drops the scroll state;
695// one that does not, keeps it.
696func TestOverviewReloadResetsScrollOnAxisChange(t *testing.T) {
697 c, _ := indicatorCore(t)
698 m := c.cur().mon
699 m.AddWindow(1)
700 m.ToggleOverview()
701 w := m.Current()
702 set := func() { m.ov.scrollX, m.ov.scrollY, m.ov.scrolled, m.ov.sideways = 20, 10, true, true }
703 set()
704 cfg := c.cfg
705 cfg.Layout.Gaps++
706 if err := c.apply(cfg); err != nil {
707 t.Fatal(err)
708 }
709 if m.ov.scrollX != 20 || !m.ov.scrolled {
710 t.Fatalf("unrelated reload reset the scroll: %+v", m.ov)
711 }
712 cfg.Layout.Overflow = string(OverflowCascade)
713 if err := c.apply(cfg); err != nil {
714 t.Fatal(err)
715 }
716 if m.Current().policy().workspace != horizontalAxis {
717 t.Fatal("setup: not cascade")
718 }
719 // The reload keeps the overview open on its selection, and cancelling
720 // still gives the original focus back.
721 if id, _ := w.Focused(); !m.ov.open || id != 1 {
722 t.Fatalf("reload lost the overview selection: open %v, focus %d", m.ov.open, id)
723 }
724 // The scroll is dropped, but not the latch that waits for the fingers
725 // to lift.
726 if m.ov.scrollX != 0 || m.ov.scrollY != 0 || !m.ov.scrolled || m.ov.sideways {
727 t.Fatalf("axis change kept the scroll or dropped the latch: %+v", m.ov)
728 }
729 set()
730 cfg.Layout.Overflow = string(OverflowFixed)
731 if err := c.apply(cfg); err != nil {
732 t.Fatal(err)
733 }
734 if m.ov.scrollX != 0 || !m.ov.scrolled {
735 t.Fatalf("workspace axis change kept the scroll or dropped the latch: %+v", m.ov)
736 }
737 m.CancelOverview()
738 if id, _ := w.Focused(); id != 1 {
739 t.Fatalf("cancel left the focus on %d", id)
740 }
741}
742
743// The finger latch outlives an axis change: a gesture that turned the
744// overview, then was turned back by a key, does not step again.
745func TestOverviewFingerLatchSurvivesAxisChange(t *testing.T) {
746 c, m := cascadeCore(t)
747 w := m.Current()
748 m.ToggleOverview()
749 c.swipeBegin(ports.SwipeBegin{Fingers: 3})
750 c.swipeUpdate(ports.SwipeUpdate{DX: 70})
751 if m.Current() != m.Workspaces[2] || m.overviewAxis() != verticalAxis {
752 t.Fatalf("setup: workspace %d", indexOf(m.Workspaces, m.Current()))
753 }
754 // Back to the cascade workspace by a key, with the fingers still down.
755 m.overviewKey(ports.KeyEvent{Keysym: "k", Pressed: true})
756 if m.Current() != w || m.overviewAxis() != horizontalAxis {
757 t.Fatal("setup: the key did not turn the overview back")
758 }
759 for _, u := range []ports.SwipeUpdate{{DY: 70}, {DX: 70}, {DY: -70}, {DX: -70}} {
760 c.swipeUpdate(u)
761 }
762 c.swipeEnd(ports.SwipeEnd{})
763 if m.Current() != w || w.Focus != 0 {
764 t.Fatalf("the gesture stepped again: workspace %d, column %d", indexOf(m.Workspaces, m.Current()), w.Focus)
765 }
766 // The next gesture steps again.
767 c.swipeBegin(ports.SwipeBegin{Fingers: 3})
768 c.swipeUpdate(ports.SwipeUpdate{DY: 70})
769 c.swipeEnd(ports.SwipeEnd{})
770 if w.band(w.Focus) != 1 {
771 t.Fatalf("the next gesture did not step: band %d", w.band(w.Focus))
772 }
773}
774
775// A stash-only cascade workspace has no columns to select: right from its
776// pile goes straight to the next workspace.
777func TestOverviewCascadeStashOnlyRight(t *testing.T) {
778 m := overviewMonitor()
779 m.Focus(1)
780 w := m.Current()
781 w.Overflow = OverflowCascade
782 w.ToggleWindowStash()
783 m.Focus(2)
784 m.AddWindow(30)
785 m.Focus(1)
786 m.ToggleOverview()
787 if len(w.Columns) != 0 || m.cardAt(w) < 0 {
788 t.Fatalf("setup: %d columns, card %d", len(w.Columns), m.cardAt(w))
789 }
790 m.OverviewMove(1, 0)
791 if m.Current() != m.Workspaces[2] {
792 t.Fatalf("right from the pile: workspace %d", indexOf(m.Workspaces, m.Current()))
793 }
794 m.OverviewMove(0, -1) // the next workspace is a scroll one: up goes back
795 if m.Current() != w || m.cardAt(w) < 0 {
796 t.Fatal("setup: back on the stash-only workspace without its card")
797 }
798 m.OverviewMove(-1, 0)
799 if m.Current() != m.Workspaces[0] {
800 t.Fatalf("left from the pile: workspace %d", indexOf(m.Workspaces, m.Current()))
801 }
802}
803
804// A cascade neighbor beside a cascade workspace draws its other lanes
805// dimmed, its selected lane level with the centre lane.
806func TestOverviewCascadeNeighborLanes(t *testing.T) {
807 m := cascadeOverview()
808 prev := m.Workspaces[0]
809 prev.Overflow = OverflowCascade
810 for id := WindowID(60); id <= 65; id++ {
811 prev.AddWindow(id) // bands {1,2,3} {60,61,62} {63,64,65}, 65 selected
812 }
813 m.ToggleOverview()
814 ps := m.Layout()
815 centre := centreY(previewOf(t, ps, 8).Rect)
816 sel := previewOf(t, ps, 65)
817 if sel.Hidden || !sel.Peek || sel.Focused || !within1(centreY(sel.Rect), centre) {
818 t.Fatalf("selected lane of the neighbor: %+v, centre lane %d", sel, centre)
819 }
820 above := previewOf(t, ps, 62)
821 if above.Hidden || !above.Peek || above.Rect.Y+above.Rect.H > sel.Rect.Y {
822 t.Fatalf("lane above in the neighbor: %+v, selected %v", above, sel.Rect)
823 }
824 if p := previewOf(t, ps, 3); !hiddenOff(p) {
825 t.Fatalf("lane two away in the neighbor: %+v", p)
826 }
827}
828
829// A scroll neighbor wider than its slot scrolls inside it: what leaves the
830// slot is hidden, so it never sits on the current slot.
831func TestOverviewCascadeWideScrollNeighborIsClipped(t *testing.T) {
832 m := cascadeOverview()
833 prev := m.Workspaces[0]
834 for id := WindowID(60); id <= 67; id++ {
835 prev.AddWindow(id)
836 }
837 m.ToggleOverview()
838 rows, _ := m.overviewRows()
839 cur, slot := rows[m.Current()], rows[prev]
840 hidden, shown := 0, 0
841 for _, p := range m.Layout() {
842 if !prev.has(p.ID) {
843 continue
844 }
845 if p.Hidden {
846 hidden++
847 continue
848 }
849 shown++
850 if p.Rect.X < slot.X || p.Rect.X+p.Rect.W > slot.X+slot.W || p.Rect.X+p.Rect.W > cur.X {
851 t.Fatalf("tile %d leaves its slot %v or reaches the current slot %v: %v", p.ID, slot, cur, p.Rect)
852 }
853 }
854 if hidden == 0 || shown == 0 {
855 t.Fatalf("setup: %d tiles shown, %d hidden", shown, hidden)
856 }
857}
858
859// A wheel and a continuous scroll in a cascade overview: vertical changes
860// workspace (and does not bounce back on a workspace with another axis),
861// horizontal moves along the band.
862func TestOverviewCascadeScroll(t *testing.T) {
863 vertical := ports.ScrollAxis{Set: true, Value: overviewScrollStep, V120: 120}
864 for _, source := range []ports.AxisSource{ports.AxisWheel, ports.AxisContinuous} {
865 t.Run(fmt.Sprint(source), func(t *testing.T) {
866 m := cascadeOverview()
867 w := m.Current()
868 m.ToggleOverview()
869 m.overviewScroll(ports.PointerAxis{Source: source, Vertical: vertical})
870 if m.Current() != m.Workspaces[2] || w.band(w.Focus) != 1 || m.ov.scrollX != 0 || m.ov.scrollY != 0 {
871 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)
872 }
873
874 m = cascadeOverview()
875 w = m.Current()
876 w.FocusID(7)
877 m.ToggleOverview()
878 m.overviewScroll(ports.PointerAxis{Source: source, Horizontal: vertical})
879 if m.Current() != w || w.Columns[w.Focus].Windows[0] != 8 {
880 t.Fatalf("horizontal: column %d", w.Columns[w.Focus].Windows[0])
881 }
882
883 // Onto a workspace with another axis: one step, no bounce.
884 m = overviewMonitor()
885 m.SetOverflow(OverflowFixed)
886 m.Workspaces[1].Overflow = OverflowCascade
887 m.ToggleOverview()
888 m.overviewScroll(ports.PointerAxis{Source: source, Vertical: vertical})
889 if m.Current() != m.Workspaces[1] || m.ov.scrollX != 0 || m.ov.scrollY != 0 {
890 t.Fatalf("mixed: workspace %d, scroll %v,%v", indexOf(m.Workspaces, m.Current()), m.ov.scrollX, m.ov.scrollY)
891 }
892 })
893 }
894}