1package core
2
3import (
4 "reflect"
5 "testing"
6)
7
8// cellsWorkspace is n columns on an 800 px wide output, no gaps, so a cell
9// of max-columns k is 800/k px.
10func cellsWorkspace(o Overflow, k, n int) *Workspace {
11 w := &Workspace{Overflow: o, MaxColumns: k}
12 w.SetOutput(800, 600)
13 for id := 1; id <= n; id++ {
14 w.AddWindow(WindowID(id))
15 }
16 return w
17}
18
19func cellsOf(w *Workspace) []int {
20 got := make([]int, len(w.Columns))
21 for i, c := range w.Columns {
22 got[i] = c.Cells
23 }
24 return got
25}
26
27func widthsOf(w *Workspace) []int {
28 got := make([]int, len(w.Columns))
29 for i, r := range w.columnRects() {
30 got[i] = r.W
31 }
32 return got
33}
34
35// The grow cycle of cycle-column-width, in the three overflows (AC1 to AC4).
36func TestGrowColumnCycle(t *testing.T) {
37 type step struct {
38 cells []int
39 widths []int // nil: not checked
40 }
41 for _, tc := range []struct {
42 name string
43 o Overflow
44 k, n int
45 focus int // window ID
46 steps []step
47 }{
48 {"cascade two columns", OverflowCascade, 4, 2, 2, []step{
49 {[]int{1, 3}, []int{200, 600}},
50 {[]int{0, 0}, []int{400, 400}},
51 {[]int{1, 3}, []int{200, 600}},
52 }},
53 {"fixed two columns", OverflowFixed, 4, 2, 2, []step{
54 {[]int{1, 3}, []int{200, 600}},
55 {[]int{0, 0}, []int{400, 400}},
56 }},
57 {"cascade three columns", OverflowCascade, 4, 3, 2, []step{
58 {[]int{1, 2, 1}, []int{200, 400, 200}},
59 {[]int{0, 0, 0}, []int{266, 266, 268}},
60 }},
61 {"fixed three columns", OverflowFixed, 4, 3, 2, []step{
62 {[]int{1, 2, 1}, []int{200, 400, 200}},
63 {[]int{0, 0, 0}, []int{266, 266, 268}},
64 }},
65 {"remainder goes to the focused column and is the step", OverflowCascade, 5, 3, 1, []step{
66 // 5 cells over 3 columns: 1 each and the 2 left to the focused one.
67 {[]int{3, 1, 1}, nil},
68 // No column has a cell to give: equal shares again.
69 {[]int{0, 0, 0}, nil},
70 {[]int{3, 1, 1}, nil},
71 }},
72 {"an even lane steps at once, the nearest tied donor first", OverflowCascade, 6, 3, 3, []step{
73 {[]int{2, 1, 3}, nil},
74 {[]int{1, 1, 4}, nil},
75 {[]int{0, 0, 0}, nil},
76 }},
77 {"scroll unset column", OverflowScroll, 4, 2, 2, []step{
78 {[]int{0, 3}, []int{400, 600}},
79 {[]int{0, 4}, []int{400, 800}},
80 {[]int{0, 0}, []int{400, 400}},
81 {[]int{0, 3}, []int{400, 600}},
82 }},
83 {"scroll lone column already fills the width", OverflowScroll, 4, 1, 1, []step{
84 {[]int{0}, []int{800}},
85 }},
86 {"scroll three columns", OverflowScroll, 4, 3, 1, []step{
87 {[]int{2, 0, 0}, nil},
88 {[]int{3, 0, 0}, nil},
89 {[]int{4, 0, 0}, nil},
90 {[]int{0, 0, 0}, nil},
91 }},
92 } {
93 t.Run(tc.name, func(t *testing.T) {
94 w := cellsWorkspace(tc.o, tc.k, tc.n)
95 w.FocusID(WindowID(tc.focus))
96 for i, s := range tc.steps {
97 w.Apply(ActionCycleColumnWidth)
98 if got := cellsOf(w); !reflect.DeepEqual(got, s.cells) {
99 t.Fatalf("step %d: cells %v, want %v", i, got, s.cells)
100 }
101 if s.widths != nil {
102 if got := widthsOf(w); !reflect.DeepEqual(got, s.widths) {
103 t.Fatalf("step %d: widths %v, want %v", i, got, s.widths)
104 }
105 }
106 }
107 })
108 }
109}
110
111// A lone column and a full lane do not change (AC3).
112func TestGrowColumnNoop(t *testing.T) {
113 for _, tc := range []struct {
114 name string
115 o Overflow
116 k, n int
117 }{
118 {"cascade lone", OverflowCascade, 4, 1},
119 {"fixed lone", OverflowFixed, 4, 1},
120 {"fixed full row", OverflowFixed, 4, 4},
121 {"fixed spiral", OverflowFixed, 2, 4},
122 {"max-columns 1", OverflowCascade, 1, 3},
123 } {
124 t.Run(tc.name, func(t *testing.T) {
125 w := cellsWorkspace(tc.o, tc.k, tc.n)
126 before := w.columnRects()
127 for _, a := range []Action{ActionCycleColumnWidth, "set-column-width +1", "set-column-width -1"} {
128 w.Apply(a)
129 if got := cellsOf(w); !reflect.DeepEqual(got, make([]int, tc.n)) {
130 t.Fatalf("%s: cells %v", a, got)
131 }
132 if got := w.columnRects(); !reflect.DeepEqual(got, before) {
133 t.Fatalf("%s: rects %v, want %v", a, got, before)
134 }
135 }
136 })
137 }
138}
139
140// set-column-width +N/-N grows or shrinks by cells without cycling, and a
141// column never goes below one cell (AC5).
142func TestSetColumnWidthCells(t *testing.T) {
143 for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
144 t.Run(string(o), func(t *testing.T) {
145 w := cellsWorkspace(o, 4, 2)
146 w.FocusID(2)
147 for _, s := range []struct {
148 a Action
149 cells []int
150 }{
151 {"set-column-width +1", []int{1, 3}},
152 {"set-column-width +1", []int{1, 3}}, // no donor: no cycle
153 {"set-column-width -1", []int{2, 2}},
154 {"set-column-width -1", []int{3, 1}},
155 {"set-column-width -1", []int{3, 1}}, // never below one cell
156 {"set-column-width +2", []int{1, 3}},
157 {"set-column-width -3", []int{3, 1}},
158 } {
159 w.Apply(s.a)
160 if got := cellsOf(w); !reflect.DeepEqual(got, s.cells) {
161 t.Fatalf("%s: cells %v, want %v", s.a, got, s.cells)
162 }
163 }
164 // The leftmost column at the lane's end gives to its left neighbor.
165 w.FocusID(1)
166 w.Apply("set-column-width -1")
167 if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2}) {
168 t.Fatalf("cells %v", got)
169 }
170 })
171 }
172 t.Run("shrink gives to the right, or to the left at the end", func(t *testing.T) {
173 w := cellsWorkspace(OverflowCascade, 6, 3)
174 w.Columns[0].Cells, w.Columns[1].Cells, w.Columns[2].Cells = 2, 2, 2
175 w.FocusID(2)
176 w.Apply("set-column-width -1")
177 if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 1, 3}) {
178 t.Fatalf("cells %v", got)
179 }
180 w.FocusID(3)
181 w.Apply("set-column-width -1")
182 if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2, 2}) {
183 t.Fatalf("cells %v", got)
184 }
185 })
186 t.Run("unnormalized lane: the shrink gives the remainder to the leftmost", func(t *testing.T) {
187 w := cellsWorkspace(OverflowCascade, 5, 3)
188 w.FocusID(3)
189 w.Apply("set-column-width -1") // base 2,2,1 and column 3 has one cell
190 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0}) {
191 t.Fatalf("cells %v: nothing is written when nothing changes", got)
192 }
193 w.FocusID(2)
194 w.Apply("set-column-width -1") // 2,2,1 -> 2,1,2
195 if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 1, 2}) {
196 t.Fatalf("cells %v", got)
197 }
198 })
199}
200
201func TestGrowColumnScrollBounds(t *testing.T) {
202 w := cellsWorkspace(OverflowScroll, 4, 2)
203 w.FocusID(2)
204 w.Apply("set-column-width -1") // auto is 2 cells
205 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 1}) {
206 t.Fatalf("cells %v", got)
207 }
208 for range 3 {
209 w.Apply("set-column-width -1")
210 }
211 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 1}) {
212 t.Fatalf("below one cell: %v", got)
213 }
214 for range 6 {
215 w.Apply("set-column-width +1")
216 }
217 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 4}) {
218 t.Fatalf("above max-columns: %v", got)
219 }
220}
221
222func TestGrowColumnEdgeCases(t *testing.T) {
223 t.Run("a float focused changes nothing", func(t *testing.T) {
224 for _, o := range []Overflow{OverflowScroll, OverflowCascade, OverflowFixed} {
225 w := cellsWorkspace(o, 4, 2)
226 w.AddFloating(9, 100, 100)
227 w.Apply(ActionCycleColumnWidth)
228 w.Apply("set-column-width +1")
229 w.Apply("set-column-width -1")
230 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0}) {
231 t.Fatalf("%s: cells %v", o, got)
232 }
233 }
234 })
235 t.Run("a maximized column unmaximizes, then grows", func(t *testing.T) {
236 for _, o := range []Overflow{OverflowScroll, OverflowCascade, OverflowFixed} {
237 w := cellsWorkspace(o, 4, 2)
238 w.ToggleFullWidth()
239 w.Apply(ActionCycleColumnWidth)
240 if w.Columns[w.Focus].FullWidth || w.Columns[w.Focus].Cells != 3 {
241 t.Fatalf("%s: %+v", o, w.Columns)
242 }
243 }
244 })
245 t.Run("a fullscreen window hides the layout: no edit", func(t *testing.T) {
246 w := cellsWorkspace(OverflowFixed, 4, 2)
247 w.ToggleFullscreen()
248 w.Apply(ActionCycleColumnWidth)
249 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0}) {
250 t.Fatalf("cells %v", got)
251 }
252 })
253 t.Run("the donor is the largest, then the nearest, right first", func(t *testing.T) {
254 w := cellsWorkspace(OverflowCascade, 5, 3)
255 w.Columns[0].Cells, w.Columns[1].Cells, w.Columns[2].Cells = 2, 1, 2
256 w.FocusID(2)
257 w.Apply(ActionCycleColumnWidth)
258 if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2, 1}) {
259 t.Fatalf("tie: cells %v", got)
260 }
261 // C is focused with {2,2,1}: A and B tie, the nearer one gives.
262 w.FocusID(3)
263 w.Apply(ActionCycleColumnWidth)
264 if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 1, 2}) {
265 t.Fatalf("largest: cells %v", got)
266 }
267 w.Columns[0].Cells, w.Columns[1].Cells, w.Columns[2].Cells = 3, 1, 1
268 w.FocusID(2)
269 w.Apply(ActionCycleColumnWidth)
270 if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2, 1}) {
271 t.Fatalf("largest: cells %v", got)
272 }
273 })
274 t.Run("only the focused band changes", func(t *testing.T) {
275 w := cellsWorkspace(OverflowCascade, 4, 6)
276 // Band 1 holds columns 5 and 6.
277 w.Apply(ActionCycleColumnWidth)
278 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0, 0, 1, 3}) {
279 t.Fatalf("cells %v", got)
280 }
281 if got := widthsOf(w); !reflect.DeepEqual(got, []int{200, 200, 200, 200, 200, 600}) {
282 t.Fatalf("widths %v", got)
283 }
284 w.Apply(ActionCycleColumnWidth)
285 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0, 0, 0, 0}) {
286 t.Fatalf("cells %v", got)
287 }
288 w.FocusID(1) // A full band pushes a neighbor down (cascade_cycle_test).
289 w.Apply(ActionCycleColumnWidth)
290 if got := cellsOf(w)[4:]; !reflect.DeepEqual(got, []int{0, 0}) {
291 t.Fatalf("other band changed: cells %v", cellsOf(w))
292 }
293 })
294}
295
296// A column set above max-columns (moved to a narrower workspace, or after a
297// reload) renders proportionally and cycles back to auto.
298func TestCellsAboveMaxColumns(t *testing.T) {
299 t.Run("lane", func(t *testing.T) {
300 for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
301 w := cellsWorkspace(o, 2, 2)
302 w.Columns[0].Cells = 3
303 if got := widthsOf(w); !reflect.DeepEqual(got, []int{600, 200}) {
304 t.Fatalf("%s: widths %v", o, got)
305 }
306 if o == OverflowCascade {
307 continue
308 }
309 // A narrower K does not rewrite the stored cells.
310 if got := cellsOf(w); !reflect.DeepEqual(got, []int{3, 0}) {
311 t.Fatalf("cells %v", got)
312 }
313 }
314 })
315 t.Run("a full uneven lane cycles back to equal", func(t *testing.T) {
316 for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
317 w := cellsWorkspace(o, 4, 4)
318 w.Columns[1].Cells = 2
319 w.Columns[0].Cells, w.Columns[2].Cells = 1, 1
320 w.Apply("set-column-width +1")
321 if got := cellsOf(w); !reflect.DeepEqual(got, []int{1, 2, 1, 0}) {
322 t.Fatalf("%s: set-column-width cycled: %v", o, got)
323 }
324 w.Apply(ActionCycleColumnWidth)
325 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0, 0}) {
326 t.Fatalf("%s: cells %v", o, got)
327 }
328 }
329 })
330 t.Run("a lone column with cells cycles back to auto", func(t *testing.T) {
331 for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
332 w := cellsWorkspace(o, 4, 1)
333 w.Columns[0].Cells = 2
334 w.Apply(ActionCycleColumnWidth)
335 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0}) {
336 t.Fatalf("%s: cells %v", o, got)
337 }
338 }
339 })
340 t.Run("scroll", func(t *testing.T) {
341 w := cellsWorkspace(OverflowScroll, 2, 2)
342 w.Columns[0].Cells = 3
343 w.FocusID(1)
344 if got := widthsOf(w)[0]; got != 800 {
345 t.Fatalf("width %d, at most max-columns cells", got)
346 }
347 w.Apply(ActionCycleColumnWidth)
348 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0}) {
349 t.Fatalf("cells %v", got)
350 }
351 })
352 t.Run("a moved column keeps its cells", func(t *testing.T) {
353 src := cellsWorkspace(OverflowScroll, 4, 2)
354 src.Columns[1].Cells = 3
355 dst := cellsWorkspace(OverflowScroll, 2, 1)
356 col, ok := src.takeColumn()
357 if !ok {
358 t.Fatal("no column")
359 }
360 dst.receive(col, 1)
361 if got := cellsOf(dst); !reflect.DeepEqual(got, []int{0, 3}) {
362 t.Fatalf("cells %v", got)
363 }
364 })
365}
366
367// The overview blocks the grow and resize binds, like the other window
368// mutations, and its cascade preview keeps the columns' cells.
369func TestOverviewKeepsCells(t *testing.T) {
370 m := newMonitor("", "")
371 m.SetOutput(900, 600)
372 m.SetOverflow(OverflowCascade)
373 m.SetMaxColumns(4)
374 for id := WindowID(1); id <= 2; id++ {
375 m.AddWindow(id)
376 }
377 w := m.Current()
378 w.Columns[0].Cells, w.Columns[1].Cells = 1, 3
379 screen := w.columnRects()
380 if screen[0].W != 225 || screen[1].W != 675 {
381 t.Fatalf("screen widths %v, want 225 and 675", screen)
382 }
383 m.ToggleOverview()
384 tiles, _, _ := w.previewTiles()
385 for i, p := range tiles {
386 if want := screen[i]; p.Rect.W != want.W || p.Rect.X != want.X-w.Usable.X || p.Rect.H != want.H {
387 t.Fatalf("column %d: preview %+v, screen %+v", i, p.Rect, want)
388 }
389 }
390 for _, a := range []Action{ActionCycleColumnWidth, "set-column-width +1", "set-column-width -1"} {
391 m.Apply(a)
392 if got := cellsOf(w); !reflect.DeepEqual(got, []int{1, 3}) {
393 t.Fatalf("%s in the overview: cells %v", a, got)
394 }
395 }
396}