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