1package core
2
3import (
4 "math"
5 "testing"
6
7 "github.com/bnema/neferwl/internal/ports"
8)
9
10// overviewMonitor has workspace 1 with columns 1, 2, 3 (a third of the
11// width each) on a 300x200 output, and workspace 2 with column 4.
12func overviewMonitor() *Monitor {
13 m := newMonitor("", "")
14 m.SetOutput(300, 200)
15 m.SetMaxColumns(3)
16 for id := WindowID(1); id <= 3; id++ {
17 m.AddWindow(id)
18 }
19 m.Focus(1)
20 m.AddWindow(4)
21 m.Focus(0)
22 return m
23}
24
25func previewOf(t *testing.T, ps []Placement, id WindowID) Placement {
26 t.Helper()
27 for _, p := range ps {
28 if p.ID == id {
29 return p
30 }
31 }
32 t.Fatalf("no placement for %d", id)
33 return Placement{}
34}
35
36// Previews keep each tile's shape: width and height shrink by the same
37// factor, and the row fits the usable width.
38func TestOverviewPreviewsKeepAspect(t *testing.T) {
39 m := overviewMonitor()
40 normal := previewOf(t, m.Layout(), 2).Rect
41 m.ToggleOverview()
42 ps := m.Layout()
43 for id := WindowID(1); id <= 3; id++ {
44 p := previewOf(t, ps, id)
45 if p.Hidden || p.Preview <= 0 || p.Peek {
46 t.Fatalf("%d: %+v", id, p)
47 }
48 if p.Rect.X < 0 || p.Rect.X+p.Rect.W > 300 {
49 t.Fatalf("%d off screen: %+v", id, p.Rect)
50 }
51 }
52 p := previewOf(t, ps, 2)
53 got, want := float64(p.Rect.W)/float64(p.Rect.H), float64(normal.W)/float64(normal.H)
54 if math.Abs(got-want) > 0.02 {
55 t.Fatalf("aspect %v, want %v (%+v)", got, want, p.Rect)
56 }
57 if math.Abs(float64(p.Rect.W)-float64(normal.W)*p.Preview) > 1 {
58 t.Fatalf("width %d, zoom %v of %d", p.Rect.W, p.Preview, normal.W)
59 }
60 // Workspace 2 is below, dimmed and unfocused.
61 below := previewOf(t, ps, 4)
62 if below.Hidden || !below.Peek || below.Focused || below.Rect.Y <= p.Rect.Y+p.Rect.H {
63 t.Fatalf("below %+v, current %+v", below, p)
64 }
65}
66
67// Many columns shrink down to the floor, then the row scrolls to keep
68// the selected column on screen.
69func TestOverviewWideWorkspaceScrolls(t *testing.T) {
70 m := newMonitor("", "")
71 m.SetOutput(300, 200)
72 m.SetMaxColumns(1)
73 for id := WindowID(1); id <= 10; id++ {
74 m.AddWindow(id)
75 }
76 m.ToggleOverview()
77 ps := m.Layout()
78 p := previewOf(t, ps, 10)
79 if p.Preview != overviewMinZoom || !p.Focused || p.Rect.X < 0 || p.Rect.X+p.Rect.W > 300 {
80 t.Fatalf("%+v", p)
81 }
82}
83
84// h/l and j/k move the selection; Escape returns, Return keeps it.
85func TestOverviewNavigation(t *testing.T) {
86 m := overviewMonitor()
87 m.Current().FocusID(3)
88 m.ToggleOverview()
89 m.OverviewMove(-1, 0)
90 if id, _ := m.Focused(); id != 2 {
91 t.Fatalf("left: %d", id)
92 }
93 m.OverviewMove(0, 1)
94 if id, _ := m.Focused(); id != 4 || m.Active != 1 {
95 t.Fatalf("down: %d on %d", id, m.Active)
96 }
97 // The empty workspace below the last one is not entered.
98 m.OverviewMove(0, 1)
99 if m.Active != 1 {
100 t.Fatalf("entered empty workspace %d", m.Active)
101 }
102 m.CancelOverview()
103 if id, _ := m.Focused(); id != 3 || m.Active != 0 || m.ov.open {
104 t.Fatalf("cancel: %d on %d", id, m.Active)
105 }
106 m.ToggleOverview()
107 m.OverviewMove(0, 1)
108 m.ToggleOverview()
109 if id, _ := m.Focused(); id != 4 || m.Active != 1 || m.ov.open {
110 t.Fatalf("confirm: %d on %d", id, m.Active)
111 }
112}
113
114// A click picks the preview under it.
115func TestOverviewPick(t *testing.T) {
116 m := overviewMonitor()
117 m.ToggleOverview()
118 below := previewOf(t, m.Layout(), 4).Rect
119 id := overviewIn(m.Layout(), float64(below.X+1), float64(below.Y+1))
120 if id != 4 {
121 t.Fatalf("hit %d", id)
122 }
123 m.OverviewPick(id)
124 if f, _ := m.Focused(); f != 4 || m.ov.open {
125 t.Fatalf("pick: %d", f)
126 }
127}
128
129// gameMonitor has, on a 300x200 output, window 1 on workspace 1 and, on
130// workspace 2, a floating game 2 (fixed size) fullscreen; workspace 1 is
131// on screen.
132func gameMonitor(o Overflow) *Monitor {
133 m := newMonitor("", "")
134 m.SetOutput(300, 200)
135 m.SetOverflow(o)
136 m.AddWindow(1)
137 m.Focus(1)
138 m.AddFloating(2, 300, 200)
139 m.SetFullscreen(2, true)
140 m.Focus(0)
141 return m
142}
143
144// A floating game covering its workspace shows as that row's preview: the
145// row is not empty.
146func TestOverviewFullscreenFloat(t *testing.T) {
147 for _, o := range []Overflow{OverflowFixed, OverflowScroll} {
148 t.Run(string(o), func(t *testing.T) {
149 m := gameMonitor(o)
150 m.ToggleOverview()
151 ps := m.Layout()
152 cur, game := previewOf(t, ps, 1), previewOf(t, ps, 2)
153 if game.Hidden || !game.Peek || game.Focused || game.Preview <= 0 || game.Rect.Y <= cur.Rect.Y {
154 t.Fatalf("game %+v under row %+v", game, cur)
155 }
156 // Selected below, then picked: fullscreen and focused.
157 m.OverviewMove(0, 1)
158 if g := previewOf(t, m.Layout(), 2); !g.Focused || g.Peek {
159 t.Fatalf("selected game %+v", g)
160 }
161 m.OverviewMove(-1, 0) // nothing else on its row
162 m.ToggleOverview()
163 if f, _ := m.Focused(); f != 2 || m.ov.open || m.Active != 1 || !previewOf(t, m.Layout(), 2).Fullscreen {
164 t.Fatalf("confirm: %d on %d", f, m.Active)
165 }
166 })
167 }
168}
169
170// A click picks the game; Escape from its row returns where the overview
171// opened.
172func TestOverviewFullscreenFloatPickAndCancel(t *testing.T) {
173 m := gameMonitor(OverflowFixed)
174 m.ToggleOverview()
175 r := previewOf(t, m.Layout(), 2).Rect
176 if id := overviewIn(m.Layout(), float64(r.X+1), float64(r.Y+1)); id != 2 {
177 t.Fatalf("hit %d", id)
178 }
179 m.OverviewMove(0, 1)
180 m.CancelOverview()
181 if f, _ := m.Focused(); f != 1 || m.Active != 0 {
182 t.Fatalf("cancel: %d on %d", f, m.Active)
183 }
184 m.ToggleOverview()
185 m.OverviewPick(2)
186 if f, _ := m.Focused(); f != 2 || m.ov.open {
187 t.Fatalf("pick: %d", f)
188 }
189}
190
191// The overview opens over the game too: the game is the selected row,
192// the workspace above shows dimmed, and Escape returns to the game.
193func TestOverviewOpensOverFullscreenFloat(t *testing.T) {
194 m := gameMonitor(OverflowFixed)
195 m.Focus(1)
196 m.ToggleOverview()
197 if !m.ov.open {
198 t.Fatal("overview did not open")
199 }
200 ps := m.Layout()
201 game, above := previewOf(t, ps, 2), previewOf(t, ps, 1)
202 if !game.Focused || game.Peek || game.Preview <= 0 || !above.Peek || above.Rect.Y >= game.Rect.Y {
203 t.Fatalf("game %+v, above %+v", game, above)
204 }
205 m.CancelOverview()
206 if f, _ := m.Focused(); f != 2 || m.Active != 1 || m.ov.open || !previewOf(t, m.Layout(), 2).Fullscreen {
207 t.Fatalf("cancel: %d on %d", f, m.Active)
208 }
209}
210
211// A tiled fullscreen window (fixed overflow) is its row's front card:
212// Return keeps it fullscreen; the column it hides is behind, and picking
213// it leaves fullscreen.
214func TestOverviewOpensOverFullscreenTile(t *testing.T) {
215 open := func() *Monitor {
216 m := newMonitor("", "")
217 m.SetOutput(300, 200)
218 m.SetOverflow(OverflowFixed)
219 m.AddWindow(1)
220 m.AddWindow(2)
221 m.ToggleFullscreen()
222 m.ToggleOverview()
223 return m
224 }
225 m := open()
226 game := previewOf(t, m.Layout(), 2)
227 if !m.ov.open || !game.Focused || game.Peek || !game.Fullscreen {
228 t.Fatalf("game %+v", game)
229 }
230 m.ToggleOverview()
231 if f, _ := m.Focused(); f != 2 || m.Active != 0 || !previewOf(t, m.Layout(), 2).Fullscreen {
232 t.Fatalf("confirm: %d on %d", f, m.Active)
233 }
234 m = open()
235 m.OverviewMove(-1, 0)
236 m.ToggleOverview()
237 if f, _ := m.Focused(); f != 1 || m.Current().fullscreen != 0 {
238 t.Fatalf("behind: focused %d, fullscreen %d", f, m.Current().fullscreen)
239 }
240}
241
242// Under scroll overflow a fullscreen column does not pin the row: another
243// column can be selected and picked, as scrolling would on screen.
244func TestOverviewOverScrollFullscreenPicksColumn(t *testing.T) {
245 m := newMonitor("", "")
246 m.SetOutput(300, 200)
247 m.SetOverflow(OverflowScroll)
248 m.AddWindow(1)
249 m.AddWindow(2)
250 m.SetFullscreen(2, true)
251 m.ToggleOverview()
252 if p := previewOf(t, m.Layout(), 1); p.Hidden {
253 t.Fatalf("column 1 hidden: %+v", p)
254 }
255 m.OverviewMove(-1, 0)
256 m.ToggleOverview()
257 if f, _ := m.Focused(); f != 1 {
258 t.Fatalf("focused %d", f)
259 }
260}
261
262// A neighbor row with a fullscreen game shows it in front, the columns it
263// hides behind; a dialog under it stays hidden, as for any small float.
264func TestOverviewFullscreenFloatRow(t *testing.T) {
265 m := gameMonitor(OverflowScroll)
266 m.Focus(1)
267 m.AddWindow(5)
268 m.Current().FocusID(2) // back on the game, over column 5
269 m.AddWindow(3)
270 m.AddFloating(4, 50, 50)
271 m.Focus(0)
272 m.ToggleOverview()
273 ps := m.Layout()
274 for _, id := range []WindowID{2, 3, 5} {
275 if p := previewOf(t, ps, id); p.Hidden || !p.Peek {
276 t.Fatalf("%d: %+v", id, p)
277 }
278 }
279 if p := previewOf(t, ps, 4); !p.Hidden {
280 t.Fatalf("dialog shown: %+v", p)
281 }
282 if g := previewOf(t, ps, 2); !g.Fullscreen {
283 t.Fatalf("game %+v", g)
284 }
285}
286
287// pileMonitor is overviewMonitor with 5, 6 and 7 stashed on workspace 1
288// (7 selected) and the stash hidden.
289func pileMonitor() *Monitor {
290 m := overviewMonitor()
291 w := m.Current()
292 for id := WindowID(5); id <= 7; id++ {
293 w.AddWindow(id)
294 w.FocusID(id)
295 w.ToggleWindowStash()
296 }
297 w.ToggleStashVisible()
298 w.FocusID(1)
299 return m
300}
301
302// The stash shows as a pile on the left of its row, even hidden: its
303// selected window in front, the others behind, dimmed. The row starts
304// right of it.
305func TestOverviewPileLayout(t *testing.T) {
306 m := pileMonitor()
307 m.ToggleOverview()
308 ps := m.Layout()
309 front, back := previewOf(t, ps, 7), previewOf(t, ps, 5)
310 if front.Hidden || front.Peek || front.Focused || front.Preview != overviewCardZoom {
311 t.Fatalf("front %+v", front)
312 }
313 if back.Hidden || !back.Peek || back.Rect.X <= front.Rect.X || back.Rect.Y >= front.Rect.Y {
314 t.Fatalf("back %+v, front %+v", back, front)
315 }
316 first := previewOf(t, ps, 1)
317 if !first.Focused || first.Rect.X < back.Rect.X+back.Rect.W {
318 t.Fatalf("row %+v overlaps the pile %+v", first.Rect, back.Rect)
319 }
320 // Front last: drawn and hit on top.
321 idx := func(id WindowID) int {
322 for i, p := range ps {
323 if p.ID == id {
324 return i
325 }
326 }
327 return -1
328 }
329 if idx(7) < idx(5) || idx(7) < idx(6) {
330 t.Fatal("front card drawn under the others")
331 }
332}
333
334// A stash pile does not shift a row that fits beside it: the row stays
335// centred like the same workspace without a stash.
336func TestOverviewPileKeepsRowCentred(t *testing.T) {
337 m := newMonitor("", "")
338 m.SetOutput(3000, 1200)
339 m.SetOverflow(OverflowFixed)
340 m.SetMaxColumns(3)
341 for id := WindowID(1); id <= 3; id++ {
342 m.AddWindow(id)
343 }
344 m.ToggleOverview()
345 want := previewOf(t, m.Layout(), 1).Rect
346 m.CancelOverview()
347 w := m.Current()
348 w.AddWindow(4)
349 w.FocusID(4)
350 w.ToggleWindowStash()
351 w.FocusID(1)
352 m.ToggleOverview()
353 if got := previewOf(t, m.Layout(), 1).Rect; got != want {
354 t.Fatalf("row with a stash at %+v, without at %+v", got, want)
355 }
356}
357
358// Left enters the stash pile, up/down browse it round without changing rows,
359// and right leaves it immediately. Confirm shows the chosen stash window.
360func TestOverviewPileNavigation(t *testing.T) {
361 m := pileMonitor()
362 w := m.Current()
363 m.ToggleOverview()
364 m.OverviewMove(-1, 0)
365 if at := m.cardAt(w); at != 2 {
366 t.Fatalf("entered at %d", at)
367 }
368 if f := previewOf(t, m.Layout(), 7); !f.Focused {
369 t.Fatalf("front card not selected: %+v", f)
370 }
371 if previewOf(t, m.Layout(), 1).Focused {
372 t.Fatal("column still selected")
373 }
374 m.OverviewMove(-1, 0) // left stays on the selected stash card
375 if at := m.cardAt(w); at != 2 {
376 t.Fatalf("left changed stash selection to %d", at)
377 }
378 m.OverviewMove(0, 1) // wraps round to the first entry, not the next workspace
379 if m.Current() != w || m.cardAt(w) != 0 {
380 t.Fatalf("down past the last entry: card %d, workspace changed %v", m.cardAt(w), m.Current() != w)
381 }
382 m.OverviewMove(0, -1) // and back round to the last
383 if at := m.cardAt(w); at != 2 || m.Current() != w {
384 t.Fatalf("up past the first entry: card %d, workspace changed %v", at, m.Current() != w)
385 }
386 m.OverviewMove(0, -1)
387 m.ToggleOverview()
388 if f, _ := m.Focused(); f != 6 || w.stashHidden || m.ov.open {
389 t.Fatalf("confirm on %d, hidden %v", f, w.stashHidden)
390 }
391 // Right returns to column 1 from any stash card.
392 w.FocusID(1)
393 m.ToggleOverview()
394 m.OverviewMove(-1, 0)
395 if at := m.cardAt(w); at != 1 {
396 t.Fatalf("entered at %d", at)
397 }
398 m.OverviewMove(1, 0)
399 if m.cardAt(w) >= 0 {
400 t.Fatal("still in the pile")
401 }
402 if f, _ := m.Focused(); f != 1 {
403 t.Fatalf("back on %d", f)
404 }
405 // Escape leaves the stash as it was.
406 m.OverviewMove(-1, 0)
407 m.CancelOverview()
408 if f, _ := m.Focused(); f != 1 {
409 t.Fatalf("cancel on %d", f)
410 }
411}
412
413// A click on a card shows the stash on it.
414func TestOverviewPilePick(t *testing.T) {
415 m := pileMonitor()
416 m.ToggleOverview()
417 r := previewOf(t, m.Layout(), 7).Rect
418 id := overviewIn(m.Layout(), float64(r.X+1), float64(r.Y+r.H-1))
419 if id != 7 {
420 t.Fatalf("hit %d", id)
421 }
422 m.OverviewPick(id)
423 if f, _ := m.Focused(); f != 7 || m.Current().stashHidden {
424 t.Fatalf("pick on %d", f)
425 }
426}
427
428// A workspace with only stashed windows opens with the pile selected, and
429// its neighbor's pile is dimmed and unselected.
430func TestOverviewPileOnly(t *testing.T) {
431 m := newMonitor("", "")
432 m.SetOutput(300, 200)
433 m.AddWindow(1)
434 w := m.Current()
435 w.ToggleWindowStash()
436 m.ToggleOverview()
437 if p := previewOf(t, m.Layout(), 1); !p.Focused || p.Hidden {
438 t.Fatalf("%+v", p)
439 }
440 m.CancelOverview()
441 m.Focus(1)
442 m.AddWindow(2)
443 m.Focus(0)
444 m.ToggleOverview()
445 m.OverviewMove(0, 1)
446 if m.Current() != w || m.cardAt(w) != 0 {
447 t.Fatal("vertical navigation escaped a stash-only workspace")
448 }
449 m.OverviewMove(1, 0)
450 if m.cardAt(w) >= 0 || m.ov.selected != 0 {
451 t.Fatal("right did not deselect the stash-only pile")
452 }
453 m.OverviewMove(0, 1)
454 if m.Current() == w {
455 t.Fatal("vertical navigation after leaving the pile did not change workspace")
456 }
457 m.CancelOverview()
458 m.Focus(1)
459 m.ToggleOverview()
460 if p := previewOf(t, m.Layout(), 1); p.Focused || !p.Peek || p.Hidden {
461 t.Fatalf("neighbor pile %+v", p)
462 }
463}
464
465// Fixed overflow previews use the actual buffer geometry, including the
466// spiral and window stacks, rather than laying full-height columns in a row.
467func TestOverviewFixedOverflowGeometry(t *testing.T) {
468 for _, cells := range []bool{false, true} {
469 t.Run(map[bool]string{false: "spiral", true: "cells ignored"}[cells], func(t *testing.T) {
470 m := newMonitor("", "")
471 m.SetOutput(600, 400)
472 m.SetOverflow(OverflowFixed)
473 m.SetMaxColumns(3)
474 for id := WindowID(1); id <= 5; id++ {
475 m.AddWindow(id)
476 }
477 w := m.Current()
478 if cells {
479 // The spiral ignores cells: the preview must still match.
480 w.Columns[0].Cells, w.Columns[2].Cells = 1, 3
481 }
482 w.Columns[3].Windows = append(w.Columns[3].Windows, 6)
483 w.Columns[3].Focus = 1
484 w.FocusID(6)
485 screen := w.Layout()
486 m.ToggleOverview()
487 if _, span, _ := w.previewTiles(); span != w.Usable.W {
488 t.Fatalf("span %d, usable %d", span, w.Usable.W)
489 }
490 ps := m.Layout()
491 z := previewOf(t, ps, 6).Preview
492 scale := func(v int) int { return int(math.Round(float64(v) * z)) }
493 anchor, previewAnchor := previewOf(t, screen, 1).Rect, previewOf(t, ps, 1).Rect
494 for _, id := range []WindowID{1, 2, 3, 4, 5, 6} {
495 r, p := previewOf(t, screen, id).Rect, previewOf(t, ps, id)
496 if p.Hidden || p.Preview != z || math.Abs(float64(p.Rect.W-scale(r.W))) > 1 || math.Abs(float64(p.Rect.H-scale(r.H))) > 1 || math.Abs(float64(p.Rect.X-previewAnchor.X-scale(r.X-anchor.X))) > 1 || math.Abs(float64(p.Rect.Y-previewAnchor.Y-scale(r.Y-anchor.Y))) > 1 {
497 t.Fatalf("%d: screen %+v preview %+v anchor %+v %+v", id, r, p, anchor, previewAnchor)
498 }
499 if p.Focused != (id == 6) {
500 t.Fatalf("%d focus: %+v", id, p)
501 }
502 for _, other := range []WindowID{1, 2, 3, 4, 5, 6} {
503 if other > id && p.Rect.Overlaps(previewOf(t, ps, other).Rect) {
504 t.Fatalf("%d overlaps %d", id, other)
505 }
506 }
507 if p.Rect.W > 0 && p.Rect.H > 0 && overviewIn(m.Layout(), float64(p.Rect.X+p.Rect.W/2), float64(p.Rect.Y+p.Rect.H/2)) != id {
508 t.Fatalf("%d not hit at %+v", id, p.Rect)
509 }
510 }
511 if len(overviewOutline(nil, ps, 2)) != 4 {
512 t.Fatal("missing selection outline")
513 }
514 m.OverviewMove(-1, 0)
515 if id, _ := m.Focused(); id != 3 {
516 t.Fatalf("left selected %d", id)
517 }
518 m.OverviewMove(1, 0)
519 if id, _ := m.Focused(); id != 6 {
520 t.Fatalf("right selected %d", id)
521 }
522 })
523 }
524}
525
526func TestOverviewFixedFullWidthShowsHiddenColumnsCard(t *testing.T) {
527 m := newMonitor("", "")
528 m.SetOutput(600, 400)
529 m.SetOverflow(OverflowFixed)
530 m.SetMaxColumns(3)
531 for id := WindowID(1); id <= 5; id++ {
532 m.AddWindow(id)
533 }
534 w := m.Current()
535 w.ToggleFullWidth()
536 m.ToggleOverview()
537 if p := previewOf(t, m.Layout(), 5); p.Hidden || p.Peek || !p.Focused {
538 t.Fatalf("front %+v", p)
539 }
540 for _, id := range []WindowID{1, 2} {
541 if p := previewOf(t, m.Layout(), id); p.Hidden || !p.Peek {
542 t.Fatalf("peek %d %+v", id, p)
543 }
544 }
545 m.OverviewMove(0, -1)
546 m.ToggleOverview()
547 if id, _ := w.Focused(); id != 1 || !w.Columns[0].FullWidth || w.Columns[4].FullWidth {
548 t.Fatalf("transfer %d %+v", id, w.Columns)
549 }
550}
551
552// The selected card follows its window: removing an earlier stash entry
553// keeps it, removing the card itself drops the selection to the column.
554// The card is what Focused reports, and close-window closes it.
555func TestOverviewCardFollowsWindow(t *testing.T) {
556 m := pileMonitor()
557 w := m.Current()
558 m.ToggleOverview()
559 m.OverviewMove(-1, 0) // on 7
560 if id, _ := m.Focused(); id != 7 {
561 t.Fatalf("focused %d, want the card", id)
562 }
563 if e := m.Apply(ActionCloseWindow); e.Close != 7 {
564 t.Fatalf("close %d, want the card", e.Close)
565 }
566 m.RemoveWindow(5)
567 if id, _ := m.Focused(); id != 7 {
568 t.Fatalf("after removing 5: %d", id)
569 }
570 m.RemoveWindow(7)
571 if id, _ := m.Focused(); id != 1 || m.cardAt(w) >= 0 {
572 t.Fatalf("after removing the card: %d", id)
573 }
574}
575
576// A workspace switched by a bind does not inherit the card selection.
577func TestOverviewCardAfterBindSwitch(t *testing.T) {
578 m := pileMonitor()
579 m.Workspaces[1].AddWindow(8)
580 m.Workspaces[1].FocusID(8)
581 m.Workspaces[1].ToggleWindowStash()
582 m.Workspaces[1].ToggleStashVisible()
583 m.ToggleOverview()
584 m.OverviewMove(-1, 0)
585 m.FocusNumber(2)
586 if id, _ := m.Focused(); id != 4 {
587 t.Fatalf("focused %d after the switch", id)
588 }
589 m.ToggleOverview()
590 if !m.Workspaces[1].stashHidden {
591 t.Fatal("the other workspace's stash was shown")
592 }
593}
594
595// Picking a card wins over another stashed window's pending fullscreen.
596func TestOverviewCardOverPendingFullscreen(t *testing.T) {
597 m := pileMonitor()
598 w := m.Current()
599 w.SetFullscreen(5, true) // hidden stash: pending
600 m.ToggleOverview()
601 m.OverviewMove(-1, 0)
602 m.ToggleOverview()
603 if id, _ := m.Focused(); id != 7 || w.fullscreen != 0 {
604 t.Fatalf("focused %d, fullscreen %d", id, w.fullscreen)
605 }
606}
607
608// On a tiny output the pile leaves room for the row.
609func TestOverviewPileTinyOutput(t *testing.T) {
610 m := pileMonitor()
611 m.SetOutput(30, 20)
612 m.ToggleOverview()
613 for _, p := range m.Layout() {
614 if !p.Hidden && (p.Rect.W < 0 || p.Rect.X < 0 || p.Rect.X > 30) {
615 t.Fatalf("%+v", p)
616 }
617 }
618 if w := m.Current(); w.pileWidth() > 10 {
619 t.Fatalf("pile %d of 30", w.pileWidth())
620 }
621}
622
623// The overview opens on what has the focus: a shown stash's card, else
624// the column under a focused native float (hidden in the overview), and
625// Return keeps it. Escape gives the float its focus back.
626func TestOverviewOpensOnFocus(t *testing.T) {
627 m := pileMonitor()
628 w := m.Current()
629 w.ToggleStashVisible()
630 m.ToggleOverview()
631 if at := m.cardAt(w); at != w.stashAt {
632 t.Fatalf("opened on card %d, want the stash's %d", at, w.stashAt)
633 }
634 m.ToggleOverview()
635
636 m = overviewMonitor()
637 w = m.Current()
638 col, _ := w.Focused()
639 w.AddFloating(9, 10, 10)
640 w.FocusID(9)
641 m.ToggleOverview()
642 if f, _ := m.Focused(); f != col || !previewOf(t, m.Layout(), col).Focused {
643 t.Fatalf("opened on %d, want column %d", f, col)
644 }
645 m.CancelOverview()
646 if f, _ := m.Focused(); f != 9 {
647 t.Fatalf("escape on %d, want the float", f)
648 }
649 w.FocusID(9)
650 m.ToggleOverview()
651 m.ToggleOverview()
652 if f, _ := m.Focused(); f != col {
653 t.Fatalf("return on %d, want column %d", f, col)
654 }
655}
656
657// Scrolling adds up to steps: what is left when the overview closes does
658// not count when it opens again, and a wheel frame of two notches moves
659// twice.
660func TestOverviewScrollSteps(t *testing.T) {
661 m := overviewMonitor()
662 w := m.Current()
663 finger := func(dx float64) ports.PointerAxis {
664 return ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: dx}}
665 }
666 m.ToggleOverview()
667 start, _ := m.Focused()
668 if m.overviewScroll(finger(-50)) {
669 t.Fatal("a partial scroll moved")
670 }
671 m.CancelOverview()
672 m.ToggleOverview()
673 if m.overviewScroll(finger(-50)) {
674 t.Fatal("the last session's scroll carried over")
675 }
676 m.CancelOverview()
677 w.FocusID(3)
678 m.ToggleOverview()
679 if !m.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Horizontal: ports.ScrollAxis{Set: true, V120: -240}}) {
680 t.Fatal("wheel did not move")
681 }
682 if f, _ := m.Focused(); f != 1 {
683 t.Fatalf("two notches from 3 landed on %d, want 1 (started on %d)", f, start)
684 }
685}
686
687// A two-finger scroll sideways steps to the next column after
688// overviewScrollStep, then once per overviewScrollRepeat, and stays on
689// columns; up or down it steps once until the fingers lift. Reopening the
690// overview, a wheel notch and continuous scrolling are not held by it.
691func TestOverviewFingerScroll(t *testing.T) {
692 finger := func(dx, dy float64) ports.PointerAxis {
693 return ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: dx}, Vertical: ports.ScrollAxis{Set: true, Value: dy}}
694 }
695 m := overviewMonitor()
696 focused := func() WindowID { f, _ := m.Focused(); return f }
697 m.Current().FocusID(3)
698 m.ToggleOverview()
699 // 70: one column; 100 more is under the longer repeat step.
700 if !m.overviewScroll(finger(-70, 0)) || focused() != 2 {
701 t.Fatalf("first step on %d", focused())
702 }
703 if m.overviewScroll(finger(-100, 0)) {
704 t.Fatal("stepped again before overviewScrollRepeat")
705 }
706 // The fingers drift down: still on columns, no row step.
707 if m.overviewScroll(finger(0, 200)) {
708 t.Fatal("a sideways scroll stepped down")
709 }
710 if !m.overviewScroll(finger(-50, 0)) || focused() != 1 {
711 t.Fatalf("long scroll on %d, want 1", focused())
712 }
713 // The fingers still rest: a wheel notch steps one column as always.
714 if !m.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Horizontal: ports.ScrollAxis{Set: true, V120: 120}}) || focused() != 2 {
715 t.Fatalf("wheel after a sideways scroll on %d, want 2", focused())
716 }
717 // Once lifted, the next scroll starts with the short step again, and
718 // one large frame takes the short step then a repeat.
719 c := &Core{screens: []*screen{{mon: m}}}
720 c.scrollStop(ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}})
721 if !m.overviewScroll(finger(220, 0)) || focused() != 3 {
722 t.Fatalf("large frame on %d, want 3", focused())
723 }
724 // Reopening forgets a sideways scroll left without its stop.
725 m.CancelOverview()
726 m.Current().FocusID(3)
727 m.ToggleOverview()
728 if !m.overviewScroll(finger(-70, 0)) || focused() != 2 {
729 t.Fatalf("reopened sideways scroll on %d, want 2", focused())
730 }
731
732 // Vertical: one step per scroll, through the stack's cards.
733 sm := stackMonitor()
734 sm.ToggleOverview()
735 before, _ := sm.Focused()
736 if !sm.overviewScroll(finger(0, 70)) {
737 t.Fatal("first vertical step")
738 }
739 after, _ := sm.Focused()
740 if after == before {
741 t.Fatal("vertical step selected nothing new")
742 }
743 if sm.overviewScroll(finger(0, 400)) {
744 t.Fatal("the same vertical scroll stepped twice")
745 }
746 if !sm.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, V120: -120}}) {
747 t.Fatal("wheel held by the finger scroll")
748 }
749 // A sideways finger scroll does not hold a vertical wheel notch.
750 sm.CancelOverview()
751 sm.ToggleOverview()
752 sm.overviewScroll(finger(70, 0))
753 if !sm.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, V120: 120}}) {
754 t.Fatal("vertical wheel held by a sideways finger scroll")
755 }
756 sm.CancelOverview()
757 sm.ToggleOverview()
758 if !sm.overviewScroll(finger(0, 70)) {
759 t.Fatal("reopened overview still held the old scroll")
760 }
761 cont := ports.PointerAxis{Source: ports.AxisContinuous, Vertical: ports.ScrollAxis{Set: true, Value: -70}}
762 if !sm.overviewScroll(cont) {
763 t.Fatal("continuous scroll held by the finger scroll")
764 }
765}
766
767// at is the slot of w, the zero Rect if it has none.
768func (s *overviewSlots) at(w *Workspace) Rect {
769 r, _ := s.lookup(w)
770 return r
771}