v0.4.0 b6be3a8

overview_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close