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 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) // stops at the last entry, not the next workspace
379 if m.Current() != w || m.cardAt(w) != 2 {
380 t.Fatal("down escaped the stash pile")
381 }
382 m.OverviewMove(0, -1)
383 m.OverviewMove(0, -1)
384 m.OverviewMove(0, -1) // stops at the first entry
385 if at := m.cardAt(w); at != 0 || m.Current() != w {
386 t.Fatalf("up end %d, workspace changed %v", at, m.Current() != w)
387 }
388 m.OverviewMove(0, 1)
389 m.ToggleOverview()
390 if f, _ := m.Focused(); f != 6 || w.stashHidden || m.ov.open {
391 t.Fatalf("confirm on %d, hidden %v", f, w.stashHidden)
392 }
393 // Right returns to column 1 from any stash card.
394 w.FocusID(1)
395 m.ToggleOverview()
396 m.OverviewMove(-1, 0)
397 if at := m.cardAt(w); at != 1 {
398 t.Fatalf("entered at %d", at)
399 }
400 m.OverviewMove(1, 0)
401 if m.cardAt(w) >= 0 {
402 t.Fatal("still in the pile")
403 }
404 if f, _ := m.Focused(); f != 1 {
405 t.Fatalf("back on %d", f)
406 }
407 // Escape leaves the stash as it was.
408 m.OverviewMove(-1, 0)
409 m.CancelOverview()
410 if f, _ := m.Focused(); f != 1 {
411 t.Fatalf("cancel on %d", f)
412 }
413}
414
415// A click on a card shows the stash on it.
416func TestOverviewPilePick(t *testing.T) {
417 m := pileMonitor()
418 m.ToggleOverview()
419 r := previewOf(t, m.Layout(), 7).Rect
420 id := overviewIn(m.Layout(), float64(r.X+1), float64(r.Y+r.H-1))
421 if id != 7 {
422 t.Fatalf("hit %d", id)
423 }
424 m.OverviewPick(id)
425 if f, _ := m.Focused(); f != 7 || m.Current().stashHidden {
426 t.Fatalf("pick on %d", f)
427 }
428}
429
430// A workspace with only stashed windows opens with the pile selected, and
431// its neighbor's pile is dimmed and unselected.
432func TestOverviewPileOnly(t *testing.T) {
433 m := newMonitor("", "")
434 m.SetOutput(300, 200)
435 m.AddWindow(1)
436 w := m.Current()
437 w.ToggleWindowStash()
438 m.ToggleOverview()
439 if p := previewOf(t, m.Layout(), 1); !p.Focused || p.Hidden {
440 t.Fatalf("%+v", p)
441 }
442 m.CancelOverview()
443 m.Focus(1)
444 m.AddWindow(2)
445 m.Focus(0)
446 m.ToggleOverview()
447 m.OverviewMove(0, 1)
448 if m.Current() != w || m.cardAt(w) != 0 {
449 t.Fatal("vertical navigation escaped a stash-only workspace")
450 }
451 m.OverviewMove(1, 0)
452 if m.cardAt(w) >= 0 || m.ov.selected != 0 {
453 t.Fatal("right did not deselect the stash-only pile")
454 }
455 m.OverviewMove(0, 1)
456 if m.Current() == w {
457 t.Fatal("vertical navigation after leaving the pile did not change workspace")
458 }
459 m.CancelOverview()
460 m.Focus(1)
461 m.ToggleOverview()
462 if p := previewOf(t, m.Layout(), 1); p.Focused || !p.Peek || p.Hidden {
463 t.Fatalf("neighbor pile %+v", p)
464 }
465}
466
467// Fixed overflow previews use the actual buffer geometry, including the
468// spiral and window stacks, rather than laying full-height columns in a row.
469func TestOverviewFixedOverflowGeometry(t *testing.T) {
470 for _, cells := range []bool{false, true} {
471 t.Run(map[bool]string{false: "spiral", true: "cells ignored"}[cells], func(t *testing.T) {
472 m := newMonitor("", "")
473 m.SetOutput(600, 400)
474 m.SetOverflow(OverflowFixed)
475 m.SetMaxColumns(3)
476 for id := WindowID(1); id <= 5; id++ {
477 m.AddWindow(id)
478 }
479 w := m.Current()
480 if cells {
481 // The spiral ignores cells: the preview must still match.
482 w.Columns[0].Cells, w.Columns[2].Cells = 1, 3
483 }
484 w.Columns[3].Windows = append(w.Columns[3].Windows, 6)
485 w.Columns[3].Focus = 1
486 w.FocusID(6)
487 screen := w.Layout()
488 m.ToggleOverview()
489 if _, span, _ := w.previewTiles(); span != w.Usable.W {
490 t.Fatalf("span %d, usable %d", span, w.Usable.W)
491 }
492 ps := m.Layout()
493 z := previewOf(t, ps, 6).Preview
494 scale := func(v int) int { return int(math.Round(float64(v) * z)) }
495 anchor, previewAnchor := previewOf(t, screen, 1).Rect, previewOf(t, ps, 1).Rect
496 for _, id := range []WindowID{1, 2, 3, 4, 5, 6} {
497 r, p := previewOf(t, screen, id).Rect, previewOf(t, ps, id)
498 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 {
499 t.Fatalf("%d: screen %+v preview %+v anchor %+v %+v", id, r, p, anchor, previewAnchor)
500 }
501 if p.Focused != (id == 6) {
502 t.Fatalf("%d focus: %+v", id, p)
503 }
504 for _, other := range []WindowID{1, 2, 3, 4, 5, 6} {
505 if other > id && p.Rect.Overlaps(previewOf(t, ps, other).Rect) {
506 t.Fatalf("%d overlaps %d", id, other)
507 }
508 }
509 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 {
510 t.Fatalf("%d not hit at %+v", id, p.Rect)
511 }
512 }
513 if len(overviewOutline(ps, 2)) != 4 {
514 t.Fatal("missing selection outline")
515 }
516 m.OverviewMove(-1, 0)
517 if id, _ := m.Focused(); id != 3 {
518 t.Fatalf("left selected %d", id)
519 }
520 m.OverviewMove(1, 0)
521 if id, _ := m.Focused(); id != 6 {
522 t.Fatalf("right selected %d", id)
523 }
524 })
525 }
526}
527
528func TestOverviewFixedFullWidthShowsHiddenColumnsCard(t *testing.T) {
529 m := newMonitor("", "")
530 m.SetOutput(600, 400)
531 m.SetOverflow(OverflowFixed)
532 m.SetMaxColumns(3)
533 for id := WindowID(1); id <= 5; id++ {
534 m.AddWindow(id)
535 }
536 w := m.Current()
537 w.ToggleFullWidth()
538 m.ToggleOverview()
539 if p := previewOf(t, m.Layout(), 5); p.Hidden || p.Peek || !p.Focused {
540 t.Fatalf("front %+v", p)
541 }
542 for _, id := range []WindowID{1, 2} {
543 if p := previewOf(t, m.Layout(), id); p.Hidden || !p.Peek {
544 t.Fatalf("peek %d %+v", id, p)
545 }
546 }
547 m.OverviewMove(0, -1)
548 m.ToggleOverview()
549 if id, _ := w.Focused(); id != 1 || !w.Columns[0].FullWidth || w.Columns[4].FullWidth {
550 t.Fatalf("transfer %d %+v", id, w.Columns)
551 }
552}
553
554// The selected card follows its window: removing an earlier stash entry
555// keeps it, removing the card itself drops the selection to the column.
556// The card is what Focused reports, and close-window closes it.
557func TestOverviewCardFollowsWindow(t *testing.T) {
558 m := pileMonitor()
559 w := m.Current()
560 m.ToggleOverview()
561 m.OverviewMove(-1, 0) // on 7
562 if id, _ := m.Focused(); id != 7 {
563 t.Fatalf("focused %d, want the card", id)
564 }
565 if e := m.Apply(ActionCloseWindow); e.Close != 7 {
566 t.Fatalf("close %d, want the card", e.Close)
567 }
568 m.RemoveWindow(5)
569 if id, _ := m.Focused(); id != 7 {
570 t.Fatalf("after removing 5: %d", id)
571 }
572 m.RemoveWindow(7)
573 if id, _ := m.Focused(); id != 1 || m.cardAt(w) >= 0 {
574 t.Fatalf("after removing the card: %d", id)
575 }
576}
577
578// A workspace switched by a bind does not inherit the card selection.
579func TestOverviewCardAfterBindSwitch(t *testing.T) {
580 m := pileMonitor()
581 m.Workspaces[1].AddWindow(8)
582 m.Workspaces[1].FocusID(8)
583 m.Workspaces[1].ToggleWindowStash()
584 m.Workspaces[1].ToggleStashVisible()
585 m.ToggleOverview()
586 m.OverviewMove(-1, 0)
587 m.FocusNumber(2)
588 if id, _ := m.Focused(); id != 4 {
589 t.Fatalf("focused %d after the switch", id)
590 }
591 m.ToggleOverview()
592 if !m.Workspaces[1].stashHidden {
593 t.Fatal("the other workspace's stash was shown")
594 }
595}
596
597// Picking a card wins over another stashed window's pending fullscreen.
598func TestOverviewCardOverPendingFullscreen(t *testing.T) {
599 m := pileMonitor()
600 w := m.Current()
601 w.SetFullscreen(5, true) // hidden stash: pending
602 m.ToggleOverview()
603 m.OverviewMove(-1, 0)
604 m.ToggleOverview()
605 if id, _ := m.Focused(); id != 7 || w.fullscreen != 0 {
606 t.Fatalf("focused %d, fullscreen %d", id, w.fullscreen)
607 }
608}
609
610// On a tiny output the pile leaves room for the row.
611func TestOverviewPileTinyOutput(t *testing.T) {
612 m := pileMonitor()
613 m.SetOutput(30, 20)
614 m.ToggleOverview()
615 for _, p := range m.Layout() {
616 if !p.Hidden && (p.Rect.W < 0 || p.Rect.X < 0 || p.Rect.X > 30) {
617 t.Fatalf("%+v", p)
618 }
619 }
620 if w := m.Current(); w.pileWidth() > 10 {
621 t.Fatalf("pile %d of 30", w.pileWidth())
622 }
623}
624
625// The overview opens on what has the focus: a shown stash's card, else
626// the column under a focused native float (hidden in the overview), and
627// Return keeps it. Escape gives the float its focus back.
628func TestOverviewOpensOnFocus(t *testing.T) {
629 m := pileMonitor()
630 w := m.Current()
631 w.ToggleStashVisible()
632 m.ToggleOverview()
633 if at := m.cardAt(w); at != w.stashAt {
634 t.Fatalf("opened on card %d, want the stash's %d", at, w.stashAt)
635 }
636 m.ToggleOverview()
637
638 m = overviewMonitor()
639 w = m.Current()
640 col, _ := w.Focused()
641 w.AddFloating(9, 10, 10)
642 w.FocusID(9)
643 m.ToggleOverview()
644 if f, _ := m.Focused(); f != col || !previewOf(t, m.Layout(), col).Focused {
645 t.Fatalf("opened on %d, want column %d", f, col)
646 }
647 m.CancelOverview()
648 if f, _ := m.Focused(); f != 9 {
649 t.Fatalf("escape on %d, want the float", f)
650 }
651 w.FocusID(9)
652 m.ToggleOverview()
653 m.ToggleOverview()
654 if f, _ := m.Focused(); f != col {
655 t.Fatalf("return on %d, want column %d", f, col)
656 }
657}
658
659// Scrolling adds up to steps: what is left when the overview closes does
660// not count when it opens again, and a wheel frame of two notches moves
661// twice.
662func TestOverviewScrollSteps(t *testing.T) {
663 m := overviewMonitor()
664 w := m.Current()
665 finger := func(dx float64) ports.PointerAxis {
666 return ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: dx}}
667 }
668 m.ToggleOverview()
669 start, _ := m.Focused()
670 if m.overviewScroll(finger(-50)) {
671 t.Fatal("a partial scroll moved")
672 }
673 m.CancelOverview()
674 m.ToggleOverview()
675 if m.overviewScroll(finger(-50)) {
676 t.Fatal("the last session's scroll carried over")
677 }
678 m.CancelOverview()
679 w.FocusID(3)
680 m.ToggleOverview()
681 if !m.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Horizontal: ports.ScrollAxis{Set: true, V120: -240}}) {
682 t.Fatal("wheel did not move")
683 }
684 if f, _ := m.Focused(); f != 1 {
685 t.Fatalf("two notches from 3 landed on %d, want 1 (started on %d)", f, start)
686 }
687}
688
689// A two-finger scroll sideways steps to the next column after
690// overviewScrollStep, then once per overviewScrollRepeat, and stays on
691// columns; up or down it steps once until the fingers lift. Reopening the
692// overview, a wheel notch and continuous scrolling are not held by it.
693func TestOverviewFingerScroll(t *testing.T) {
694 finger := func(dx, dy float64) ports.PointerAxis {
695 return ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: dx}, Vertical: ports.ScrollAxis{Set: true, Value: dy}}
696 }
697 m := overviewMonitor()
698 focused := func() WindowID { f, _ := m.Focused(); return f }
699 m.Current().FocusID(3)
700 m.ToggleOverview()
701 // 70: one column; 100 more is under the longer repeat step.
702 if !m.overviewScroll(finger(-70, 0)) || focused() != 2 {
703 t.Fatalf("first step on %d", focused())
704 }
705 if m.overviewScroll(finger(-100, 0)) {
706 t.Fatal("stepped again before overviewScrollRepeat")
707 }
708 // The fingers drift down: still on columns, no row step.
709 if m.overviewScroll(finger(0, 200)) {
710 t.Fatal("a sideways scroll stepped down")
711 }
712 if !m.overviewScroll(finger(-50, 0)) || focused() != 1 {
713 t.Fatalf("long scroll on %d, want 1", focused())
714 }
715 // The fingers still rest: a wheel notch steps one column as always.
716 if !m.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Horizontal: ports.ScrollAxis{Set: true, V120: 120}}) || focused() != 2 {
717 t.Fatalf("wheel after a sideways scroll on %d, want 2", focused())
718 }
719 // Once lifted, the next scroll starts with the short step again, and
720 // one large frame takes the short step then a repeat.
721 c := &Core{screens: []*screen{{mon: m}}}
722 c.scrollStop(ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}})
723 if !m.overviewScroll(finger(220, 0)) || focused() != 3 {
724 t.Fatalf("large frame on %d, want 3", focused())
725 }
726 // Reopening forgets a sideways scroll left without its stop.
727 m.CancelOverview()
728 m.Current().FocusID(3)
729 m.ToggleOverview()
730 if !m.overviewScroll(finger(-70, 0)) || focused() != 2 {
731 t.Fatalf("reopened sideways scroll on %d, want 2", focused())
732 }
733
734 // Vertical: one step per scroll, through the stack's cards.
735 sm := stackMonitor()
736 sm.ToggleOverview()
737 before, _ := sm.Focused()
738 if !sm.overviewScroll(finger(0, 70)) {
739 t.Fatal("first vertical step")
740 }
741 after, _ := sm.Focused()
742 if after == before {
743 t.Fatal("vertical step selected nothing new")
744 }
745 if sm.overviewScroll(finger(0, 400)) {
746 t.Fatal("the same vertical scroll stepped twice")
747 }
748 if !sm.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, V120: -120}}) {
749 t.Fatal("wheel held by the finger scroll")
750 }
751 // A sideways finger scroll does not hold a vertical wheel notch.
752 sm.CancelOverview()
753 sm.ToggleOverview()
754 sm.overviewScroll(finger(70, 0))
755 if !sm.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, V120: 120}}) {
756 t.Fatal("vertical wheel held by a sideways finger scroll")
757 }
758 sm.CancelOverview()
759 sm.ToggleOverview()
760 if !sm.overviewScroll(finger(0, 70)) {
761 t.Fatal("reopened overview still held the old scroll")
762 }
763 cont := ports.PointerAxis{Source: ports.AxisContinuous, Vertical: ports.ScrollAxis{Set: true, Value: -70}}
764 if !sm.overviewScroll(cont) {
765 t.Fatal("continuous scroll held by the finger scroll")
766 }
767}