cascade_test.go RawHistoryFolder

1package core
2
3import (
4	"slices"
5	"testing"
6)
7
8func cascadeWorkspace(n int) *Workspace {
9	w := &Workspace{Overflow: OverflowCascade, MaxColumns: 3}
10	w.SetOutput(900, 600)
11	for i := 1; i <= n; i++ {
12		w.AddWindow(WindowID(i))
13	}
14	return w
15}
16
17func TestCascadeBandsAndReveal(t *testing.T) {
18	for _, n := range []int{0, 1, 3, 4, 7} {
19		w := cascadeWorkspace(n)
20		ps := w.Layout()
21		if len(ps) != n {
22			t.Fatalf("n=%d placements=%d", n, len(ps))
23		}
24		for i, p := range ps {
25			want := Rect{X: i % 3 * 300, Y: (i/3)*600 - w.View, W: 300, H: 600}
26			if p.Rect != want {
27				t.Fatalf("n=%d window=%d rect=%v want=%v", n, p.ID, p.Rect, want)
28			}
29		}
30		if n > 0 && w.View != ((n-1)/3)*600 {
31			t.Fatalf("n=%d view=%d", n, w.View)
32		}
33	}
34}
35
36func TestCascadeCompactionAndNavigation(t *testing.T) {
37	w := cascadeWorkspace(7)
38	w.FocusID(2)
39	if !w.FocusWindow(1) {
40		t.Fatal("no lower band")
41	}
42	if id, _ := w.Focused(); id != 5 {
43		t.Fatalf("focus=%d", id)
44	}
45	w.RemoveWindow(1)
46	if w.View != 600 {
47		t.Fatalf("compacted view=%d", w.View)
48	}
49	w.SetMaxColumns(2)
50	if w.View != 600 {
51		t.Fatalf("changed max view=%d", w.View)
52	}
53	w.FocusID(2)
54	w.AddWindow(8)
55	if w.Columns[len(w.Columns)-1].Windows[0] != 8 {
56		t.Fatal("new column did not append")
57	}
58	w.SetUsable(Rect{X: 0, Y: 20, W: 900, H: 580})
59	if w.View != w.band(w.Focus)*580 {
60		t.Fatal("resize did not reveal band")
61	}
62	if !slices.Equal(w.snapPoints(), []float64{0, 580, 1160, 1740}) {
63		t.Fatalf("points=%v", w.snapPoints())
64	}
65}
66
67func TestCascadeCovers(t *testing.T) {
68	w := cascadeWorkspace(4)
69	w.FocusID(2)
70	w.ToggleFullWidth()
71	ps := w.Layout()
72	for _, p := range ps {
73		if p.ID == 2 && p.Rect.W != 900 {
74			t.Fatalf("full width=%v", p.Rect)
75		}
76		if (p.ID == 1 || p.ID == 3) && !p.Hidden {
77			t.Fatalf("sibling %d visible", p.ID)
78		}
79	}
80	w.ToggleFullWidth()
81	w.ToggleFullscreen()
82	for _, p := range w.Layout() {
83		if p.ID == 2 && p.Rect != w.Output {
84			t.Fatalf("fullscreen=%v", p.Rect)
85		}
86		if p.ID != 2 && !p.Hidden {
87			t.Fatalf("fullscreen sibling %d visible", p.ID)
88		}
89	}
90	w.ToggleFullscreen()
91	w.FocusID(4)
92	if w.View != 600 {
93		t.Fatal("fullscreen restore lost band reveal")
94	}
95}
96
97func TestCascadeClearsMaximizationOnBandChanges(t *testing.T) {
98	for _, move := range []struct {
99		name string
100		run  func(*Workspace)
101	}{
102		{"focus", func(w *Workspace) { w.FocusWindow(1) }},
103		{"append", func(w *Workspace) { w.AddWindow(5) }},
104		{"insert", func(w *Workspace) { w.insertColumn(4, Column{Windows: []WindowID{5}}) }},
105	} {
106		t.Run(move.name, func(t *testing.T) {
107			w := cascadeWorkspace(4)
108			w.FocusID(1)
109			w.ToggleFullWidth()
110			move.run(w)
111			for _, col := range w.Columns {
112				if col.FullWidth {
113					t.Fatal("stale maximization")
114				}
115			}
116		})
117	}
118}
119
120func TestCascadeReloadAndStashRestoreClearMaximization(t *testing.T) {
121	w := cascadeWorkspace(3)
122	w.FocusID(1)
123	w.ToggleFullWidth()
124	w.restore(4, &origPlace{stacked: []WindowID{2}, col: 1})
125	for _, c := range w.Columns {
126		if c.FullWidth {
127			t.Fatal("restore retained maximization")
128		}
129	}
130	m := NewMonitor()
131	m.SetOutput(900, 600)
132	for i := WindowID(1); i <= 3; i++ {
133		m.AddWindow(i)
134	}
135	m.Current().FocusID(1)
136	m.Current().ToggleFullWidth()
137	m.Current().FocusID(3)
138	m.SetOverflow(OverflowCascade)
139	for _, c := range m.Current().Columns {
140		if c.FullWidth {
141			t.Fatal("reload retained maximization")
142		}
143	}
144}
145
146func TestCascadeGestureValidityDoesNotAllocate(t *testing.T) {
147	m := NewMonitor()
148	m.SetOutput(900, 600)
149	m.SetOverflow(OverflowCascade)
150	for i := WindowID(1); i <= 7; i++ {
151		m.AddWindow(i)
152	}
153	g := &swipeGesture{ws: m.Current(), points: m.Current().snapPoints(), horizontal: false, opens: m.overviewOpens}
154	if n := testing.AllocsPerRun(100, func() {
155		if g.columnsChanged(m) {
156			t.Fatal("unchanged gesture invalidated")
157		}
158	}); n != 0 {
159		t.Fatalf("allocations=%v", n)
160	}
161}
162
163func TestCascadeGeometryReusesBuffer(t *testing.T) {
164	w := cascadeWorkspace(7)
165	buf := w.columnRectsInto(nil, false)
166	if n := testing.AllocsPerRun(100, func() { buf = w.columnRectsInto(buf, false) }); n != 0 {
167		t.Fatalf("allocations=%v", n)
168	}
169	// The overview's lane-encoded tiles reuse their buffer too.
170	tiles, _, _ := w.previewTiles()
171	if n := testing.AllocsPerRun(100, func() { tiles, _, _ = w.previewTilesInto(tiles[:0]) }); n != 0 {
172		t.Fatalf("preview tile allocations=%v", n)
173	}
174}
175
176// Lanes are the bands of a cascade workspace and the whole row of any other.
177func TestLaneGeometry(t *testing.T) {
178	w := cascadeWorkspace(7) // bands {0,1,2} {3,4,5} {6}
179	for _, c := range []struct{ i, lane int }{{0, 0}, {2, 0}, {3, 1}, {6, 2}} {
180		if got := w.laneOf(c.i); got != c.lane {
181			t.Fatalf("cascade laneOf(%d)=%d, want %d", c.i, got, c.lane)
182		}
183	}
184	for lane, want := range [][2]int{{0, 2}, {3, 5}, {6, 6}} {
185		if first, last := w.laneBounds(lane); first != want[0] || last != want[1] {
186			t.Fatalf("cascade laneBounds(%d)=%d,%d, want %v", lane, first, last, want)
187		}
188	}
189	for _, lane := range []int{-1, 3} {
190		if first, last := w.laneBounds(lane); first <= last {
191			t.Fatalf("cascade laneBounds(%d)=%d,%d exists", lane, first, last)
192		}
193	}
194	// The closest column of the adjacent lane keeps its offset, clamped.
195	for _, c := range []struct{ focus, d, want int }{{1, 1, 4}, {5, 1, 6}, {4, -1, 1}, {2, -1, -1}, {6, 1, -1}, {6, -1, 3}} {
196		w.Focus = c.focus
197		if got := w.laneNeighbor(c.d); got != c.want {
198			t.Fatalf("cascade laneNeighbor from %d by %d = %d, want %d", c.focus, c.d, got, c.want)
199		}
200	}
201
202	s := &Workspace{Overflow: OverflowScroll, MaxColumns: 3}
203	s.SetOutput(900, 600)
204	for i := 1; i <= 5; i++ {
205		s.AddWindow(WindowID(i))
206	}
207	for i := range s.Columns {
208		if s.laneOf(i) != 0 {
209			t.Fatalf("scroll laneOf(%d)=%d", i, s.laneOf(i))
210		}
211	}
212	if first, last := s.laneBounds(0); first != 0 || last != 4 {
213		t.Fatalf("scroll laneBounds(0)=%d,%d", first, last)
214	}
215	for _, lane := range []int{-1, 1} {
216		if first, last := s.laneBounds(lane); first <= last {
217			t.Fatalf("scroll laneBounds(%d)=%d,%d exists", lane, first, last)
218		}
219	}
220	for _, d := range []int{-1, 1} {
221		if got := s.laneNeighbor(d); got != -1 {
222			t.Fatalf("scroll laneNeighbor(%d)=%d", d, got)
223		}
224	}
225}
226
227func TestMixedLayoutSwitchKeepsSourceAxis(t *testing.T) {
228	m := NewMonitor()
229	m.SetOutput(900, 600)
230	m.Current().Overflow = OverflowCascade
231	m.AddWindow(1)
232	m.Focus(1)
233	m.AddWindow(2)
234	m.Current().Overflow = OverflowScroll
235	m.Focus(0)
236	m.switchAxis = m.Current().policy().workspace
237	m.switchView.off = .5
238	ps := m.layoutInto(nil)
239	for _, p := range ps {
240		if p.ID == 1 && (p.Rect.X != -450 || p.Rect.Y != 0) {
241			t.Fatalf("cascade source offset=%v", p.Rect)
242		}
243	}
244	m.stopSwitch()
245	m.Focus(1)
246	m.switchAxis = m.Current().policy().workspace
247	m.switchView.off = -.5
248	ps = m.layoutInto(nil)
249	for _, p := range ps {
250		if p.ID == 2 && (p.Rect.X != 0 || p.Rect.Y != 300) {
251			t.Fatalf("scroll source offset=%v", p.Rect)
252		}
253	}
254}
255
256func TestCascadeViewportSnap(t *testing.T) {
257	w := cascadeWorkspace(7)
258	w.FocusID(3)
259	if w.snapFocus(600, true) != 5 {
260		t.Fatalf("nearest cell=%d", w.snapFocus(600, true))
261	}
262	if w.snapFocus(1200, true) != 6 {
263		t.Fatal("short band neighbour")
264	}
265	w.SetUsable(Rect{})
266	if got := w.snapPoints(); len(got) != 1 || got[0] != 0 {
267		t.Fatalf("zero area=%v", got)
268	}
269}

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