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, 266}},
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, 266}},
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 {"cascade full band", OverflowCascade, 4, 4},
121 {"cascade full bands", OverflowCascade, 3, 6},
122 {"fixed full row", OverflowFixed, 4, 4},
123 {"fixed spiral", OverflowFixed, 2, 4},
124 {"max-columns 1", OverflowCascade, 1, 3},
125 } {
126 t.Run(tc.name, func(t *testing.T) {
127 w := cellsWorkspace(tc.o, tc.k, tc.n)
128 before := w.columnRects()
129 for _, a := range []Action{ActionCycleColumnWidth, "set-column-width +1", "set-column-width -1"} {
130 w.Apply(a)
131 if got := cellsOf(w); !reflect.DeepEqual(got, make([]int, tc.n)) {
132 t.Fatalf("%s: cells %v", a, got)
133 }
134 if got := w.columnRects(); !reflect.DeepEqual(got, before) {
135 t.Fatalf("%s: rects %v, want %v", a, got, before)
136 }
137 }
138 })
139 }
140}
141
142// set-column-width +N/-N grows or shrinks by cells without cycling, and a
143// column never goes below one cell (AC5).
144func TestSetColumnWidthCells(t *testing.T) {
145 for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
146 t.Run(string(o), func(t *testing.T) {
147 w := cellsWorkspace(o, 4, 2)
148 w.FocusID(2)
149 for _, s := range []struct {
150 a Action
151 cells []int
152 }{
153 {"set-column-width +1", []int{1, 3}},
154 {"set-column-width +1", []int{1, 3}}, // no donor: no cycle
155 {"set-column-width -1", []int{2, 2}},
156 {"set-column-width -1", []int{3, 1}},
157 {"set-column-width -1", []int{3, 1}}, // never below one cell
158 {"set-column-width +2", []int{1, 3}},
159 {"set-column-width -3", []int{3, 1}},
160 } {
161 w.Apply(s.a)
162 if got := cellsOf(w); !reflect.DeepEqual(got, s.cells) {
163 t.Fatalf("%s: cells %v, want %v", s.a, got, s.cells)
164 }
165 }
166 // The leftmost column at the lane's end gives to its left neighbor.
167 w.FocusID(1)
168 w.Apply("set-column-width -1")
169 if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2}) {
170 t.Fatalf("cells %v", got)
171 }
172 })
173 }
174 t.Run("shrink gives to the right, or to the left at the end", func(t *testing.T) {
175 w := cellsWorkspace(OverflowCascade, 6, 3)
176 w.Columns[0].Cells, w.Columns[1].Cells, w.Columns[2].Cells = 2, 2, 2
177 w.FocusID(2)
178 w.Apply("set-column-width -1")
179 if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 1, 3}) {
180 t.Fatalf("cells %v", got)
181 }
182 w.FocusID(3)
183 w.Apply("set-column-width -1")
184 if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2, 2}) {
185 t.Fatalf("cells %v", got)
186 }
187 })
188 t.Run("unnormalized lane: the shrink gives the remainder to the leftmost", func(t *testing.T) {
189 w := cellsWorkspace(OverflowCascade, 5, 3)
190 w.FocusID(3)
191 w.Apply("set-column-width -1") // base 2,2,1 and column 3 has one cell
192 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0}) {
193 t.Fatalf("cells %v: nothing is written when nothing changes", got)
194 }
195 w.FocusID(2)
196 w.Apply("set-column-width -1") // 2,2,1 -> 2,1,2
197 if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 1, 2}) {
198 t.Fatalf("cells %v", got)
199 }
200 })
201}
202
203func TestGrowColumnScrollBounds(t *testing.T) {
204 w := cellsWorkspace(OverflowScroll, 4, 2)
205 w.FocusID(2)
206 w.Apply("set-column-width -1") // auto is 2 cells
207 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 1}) {
208 t.Fatalf("cells %v", got)
209 }
210 for range 3 {
211 w.Apply("set-column-width -1")
212 }
213 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 1}) {
214 t.Fatalf("below one cell: %v", got)
215 }
216 for range 6 {
217 w.Apply("set-column-width +1")
218 }
219 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 4}) {
220 t.Fatalf("above max-columns: %v", got)
221 }
222}
223
224func TestGrowColumnEdgeCases(t *testing.T) {
225 t.Run("a float focused changes nothing", func(t *testing.T) {
226 for _, o := range []Overflow{OverflowScroll, OverflowCascade, OverflowFixed} {
227 w := cellsWorkspace(o, 4, 2)
228 w.AddFloating(9, 100, 100)
229 w.Apply(ActionCycleColumnWidth)
230 w.Apply("set-column-width +1")
231 w.Apply("set-column-width -1")
232 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0}) {
233 t.Fatalf("%s: cells %v", o, got)
234 }
235 }
236 })
237 t.Run("a maximized column unmaximizes, then grows", func(t *testing.T) {
238 for _, o := range []Overflow{OverflowScroll, OverflowCascade, OverflowFixed} {
239 w := cellsWorkspace(o, 4, 2)
240 w.ToggleFullWidth()
241 w.Apply(ActionCycleColumnWidth)
242 if w.Columns[w.Focus].FullWidth || w.Columns[w.Focus].Cells != 3 {
243 t.Fatalf("%s: %+v", o, w.Columns)
244 }
245 }
246 })
247 t.Run("a fullscreen window hides the layout: no edit", func(t *testing.T) {
248 w := cellsWorkspace(OverflowFixed, 4, 2)
249 w.ToggleFullscreen()
250 w.Apply(ActionCycleColumnWidth)
251 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0}) {
252 t.Fatalf("cells %v", got)
253 }
254 })
255 t.Run("the donor is the largest, then the nearest, right first", func(t *testing.T) {
256 w := cellsWorkspace(OverflowCascade, 5, 3)
257 w.Columns[0].Cells, w.Columns[1].Cells, w.Columns[2].Cells = 2, 1, 2
258 w.FocusID(2)
259 w.Apply(ActionCycleColumnWidth)
260 if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2, 1}) {
261 t.Fatalf("tie: cells %v", got)
262 }
263 // C is focused with {2,2,1}: A and B tie, the nearer one gives.
264 w.FocusID(3)
265 w.Apply(ActionCycleColumnWidth)
266 if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 1, 2}) {
267 t.Fatalf("largest: cells %v", got)
268 }
269 w.Columns[0].Cells, w.Columns[1].Cells, w.Columns[2].Cells = 3, 1, 1
270 w.FocusID(2)
271 w.Apply(ActionCycleColumnWidth)
272 if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2, 1}) {
273 t.Fatalf("largest: cells %v", got)
274 }
275 })
276 t.Run("only the focused band changes", func(t *testing.T) {
277 w := cellsWorkspace(OverflowCascade, 4, 6)
278 // Band 1 holds columns 5 and 6.
279 w.Apply(ActionCycleColumnWidth)
280 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0, 0, 1, 3}) {
281 t.Fatalf("cells %v", got)
282 }
283 if got := widthsOf(w); !reflect.DeepEqual(got, []int{200, 200, 200, 200, 200, 600}) {
284 t.Fatalf("widths %v", got)
285 }
286 w.Apply(ActionCycleColumnWidth)
287 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0, 0, 0, 0}) {
288 t.Fatalf("cells %v", got)
289 }
290 w.FocusID(1) // a full band
291 w.Apply(ActionCycleColumnWidth)
292 if got := cellsOf(w); !reflect.DeepEqual(got, make([]int, 6)) {
293 t.Fatalf("cells %v", got)
294 }
295 })
296}
297
298// A column set above max-columns (moved to a narrower workspace, or after a
299// reload) renders proportionally and cycles back to auto.
300func TestCellsAboveMaxColumns(t *testing.T) {
301 t.Run("lane", func(t *testing.T) {
302 for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
303 w := cellsWorkspace(o, 2, 2)
304 w.Columns[0].Cells = 3
305 if got := widthsOf(w); !reflect.DeepEqual(got, []int{600, 200}) {
306 t.Fatalf("%s: widths %v", o, got)
307 }
308 if o == OverflowCascade {
309 continue
310 }
311 // A narrower K does not rewrite the stored cells.
312 if got := cellsOf(w); !reflect.DeepEqual(got, []int{3, 0}) {
313 t.Fatalf("cells %v", got)
314 }
315 }
316 })
317 t.Run("a full uneven lane cycles back to equal", func(t *testing.T) {
318 for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
319 w := cellsWorkspace(o, 4, 4)
320 w.Columns[1].Cells = 2
321 w.Columns[0].Cells, w.Columns[2].Cells = 1, 1
322 w.Apply("set-column-width +1")
323 if got := cellsOf(w); !reflect.DeepEqual(got, []int{1, 2, 1, 0}) {
324 t.Fatalf("%s: set-column-width cycled: %v", o, got)
325 }
326 w.Apply(ActionCycleColumnWidth)
327 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0, 0}) {
328 t.Fatalf("%s: cells %v", o, got)
329 }
330 }
331 })
332 t.Run("a lone column with cells cycles back to auto", func(t *testing.T) {
333 for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
334 w := cellsWorkspace(o, 4, 1)
335 w.Columns[0].Cells = 2
336 w.Apply(ActionCycleColumnWidth)
337 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0}) {
338 t.Fatalf("%s: cells %v", o, got)
339 }
340 }
341 })
342 t.Run("scroll", func(t *testing.T) {
343 w := cellsWorkspace(OverflowScroll, 2, 2)
344 w.Columns[0].Cells = 3
345 w.FocusID(1)
346 if got := widthsOf(w)[0]; got != 800 {
347 t.Fatalf("width %d, at most max-columns cells", got)
348 }
349 w.Apply(ActionCycleColumnWidth)
350 if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0}) {
351 t.Fatalf("cells %v", got)
352 }
353 })
354 t.Run("a moved column keeps its cells", func(t *testing.T) {
355 src := cellsWorkspace(OverflowScroll, 4, 2)
356 src.Columns[1].Cells = 3
357 dst := cellsWorkspace(OverflowScroll, 2, 1)
358 col, ok := src.takeColumn()
359 if !ok {
360 t.Fatal("no column")
361 }
362 dst.receive(col, 1)
363 if got := cellsOf(dst); !reflect.DeepEqual(got, []int{0, 3}) {
364 t.Fatalf("cells %v", got)
365 }
366 })
367}
368
369// The overview blocks the grow and resize binds, like the other window
370// mutations, and its cascade preview keeps the columns' cells.
371func TestOverviewKeepsCells(t *testing.T) {
372 m := newMonitor("", "")
373 m.SetOutput(900, 600)
374 m.SetOverflow(OverflowCascade)
375 m.SetMaxColumns(4)
376 for id := WindowID(1); id <= 2; id++ {
377 m.AddWindow(id)
378 }
379 w := m.Current()
380 w.Columns[0].Cells, w.Columns[1].Cells = 1, 3
381 screen := w.columnRects()
382 if screen[0].W != 225 || screen[1].W != 675 {
383 t.Fatalf("screen widths %v, want 225 and 675", screen)
384 }
385 m.ToggleOverview()
386 tiles, _, _ := w.previewTiles()
387 for i, p := range tiles {
388 if want := screen[i]; p.Rect.W != want.W || p.Rect.X != want.X-w.Usable.X || p.Rect.H != want.H {
389 t.Fatalf("column %d: preview %+v, screen %+v", i, p.Rect, want)
390 }
391 }
392 for _, a := range []Action{ActionCycleColumnWidth, "set-column-width +1", "set-column-width -1"} {
393 m.Apply(a)
394 if got := cellsOf(w); !reflect.DeepEqual(got, []int{1, 3}) {
395 t.Fatalf("%s in the overview: cells %v", a, got)
396 }
397 }
398}