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