feat/data-dnd e05cb15

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// 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}

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