1package core
2
3import (
4 "math"
5 "reflect"
6 "testing"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11func workspace() *Workspace {
12 w := &Workspace{}
13 w.SetOutput(100, 81)
14 w.SetGaps(5)
15 w.SetMaxColumns(2)
16 return w
17}
18
19// Columns of a lane share the usable width by their cells; unset columns
20// share what the others leave.
21func TestLaneWidths(t *testing.T) {
22 cells := func(w *Workspace, c ...int) []int {
23 for i, n := range c {
24 w.Columns[i].Cells = n
25 }
26 var got []int
27 for i := range w.Columns {
28 got = append(got, w.cellWidth(i))
29 }
30 return got
31 }
32 for _, overflow := range []Overflow{OverflowCascade, OverflowFixed} {
33 t.Run(string(overflow), func(t *testing.T) {
34 mk := func(n int) *Workspace {
35 w := &Workspace{MaxColumns: 4, Overflow: overflow, Gaps: 8}
36 w.SetOutput(1000, 500)
37 for id := 1; id <= n; id++ {
38 w.AddWindow(WindowID(id))
39 }
40 return w
41 }
42 avail := 1000 - 3*8
43 if got, want := cells(mk(2), 1, 3), []int{avail / 4, avail * 3 / 4}; !reflect.DeepEqual(got, want) {
44 t.Errorf("1,3: got %v want %v", got, want)
45 }
46 // Auto columns share what the set ones leave.
47 w := mk(3)
48 avail = 1000 - 4*8
49 if got, want := cells(w, 2, 0, 0), []int{avail * 2 / 4, avail / 4, avail / 4}; !reflect.DeepEqual(got, want) {
50 t.Errorf("2,auto,auto: got %v want %v", got, want)
51 }
52 // No room for them: an auto column weighs one cell.
53 if got, want := cells(w, 3, 0, 0), []int{avail * 3 / 5, avail / 5, avail - avail*3/5 - avail/5}; !reflect.DeepEqual(got, want) {
54 t.Errorf("3,auto,auto: got %v want %v", got, want)
55 }
56 // All auto: equal shares, with remaining pixels in the last column.
57 for n := 1; n <= 4; n++ {
58 w := mk(n)
59 for i := range w.Columns {
60 want := w.equalShare(n)
61 if i == n-1 {
62 want = 1000 - 8*(n+1) - (n-1)*want
63 }
64 if got := w.cellWidth(i); got != want {
65 t.Errorf("%d auto columns: width %d want %d", n, got, want)
66 }
67 }
68 }
69 // Positions follow the widths.
70 w = mk(2)
71 cells(w, 1, 3)
72 r := w.columnRects()
73 if r[0].X != 8 || r[1].X != 8+r[0].W+8 {
74 t.Errorf("rects %v", r)
75 }
76 })
77 }
78 t.Run("scroll", func(t *testing.T) {
79 w := &Workspace{MaxColumns: 4, Gaps: 8}
80 w.SetOutput(1000, 500)
81 w.AddWindow(1)
82 w.AddWindow(2)
83 w.Columns[1].Cells = 2
84 if got, want := w.columnWidth(1), 2*w.equalShare(4)+8; got != want {
85 t.Errorf("2 cells: %d want %d", got, want)
86 }
87 if got, want := w.columnWidth(0), w.equalShare(2); got != want {
88 t.Errorf("auto: %d want %d", got, want)
89 }
90 w.Columns[1].Cells = 9
91 if got, want := w.columnWidth(1), 4*w.equalShare(4)+3*8; got != want {
92 t.Errorf("above max-columns: %d want %d", got, want)
93 }
94 })
95 t.Run("fixed past max-columns ignores cells", func(t *testing.T) {
96 w := fixedWorkspace(2, 0, 3)
97 before := w.columnRects()
98 w.Columns[0].Cells = 1
99 w.Columns[1].Cells = 3
100 if after := w.columnRects(); !reflect.DeepEqual(before, after) {
101 t.Errorf("%v != %v", after, before)
102 }
103 })
104}
105func TestOperations(t *testing.T) {
106 w := workspace()
107 w.FocusColumn(1)
108 w.FocusWindow(-1)
109 w.MoveColumn(1)
110 w.GrowColumn(true)
111 w.ToggleFullscreen()
112 w.RemoveWindow(1)
113 if _, ok := w.Focused(); ok {
114 t.Fatal("empty focus")
115 }
116 w.AddWindow(1)
117 w.AddWindow(2)
118 w.AddWindow(3)
119 // Three auto columns, two visible: each (100-3*5)/2 = 42 wide.
120 if w.Focus != 2 || w.View != 46 || w.columnWidth(0) != 42 {
121 t.Fatalf("scroll: %+v", w)
122 }
123 w.FocusColumn(-1)
124 if w.View != 46 {
125 t.Fatal(w.View)
126 }
127 w.FocusColumn(-1)
128 if w.View != 0 {
129 t.Fatal(w.View)
130 }
131 w.MoveColumn(1)
132 if w.Focus != 1 || w.Columns[1].Windows[0] != 1 {
133 t.Fatal(w.Columns)
134 }
135 w.RemoveWindow(1)
136 if id, _ := w.Focused(); id != 2 {
137 t.Fatal(id)
138 }
139 w.RemoveWindow(3)
140 w.RemoveWindow(2)
141 if len(w.Columns) != 0 {
142 t.Fatal(w.Columns)
143 }
144}
145func TestStackAndLayout(t *testing.T) {
146 w := workspace()
147 w.Columns = []Column{{Windows: []WindowID{1, 2, 3}, Focus: 1}}
148 w.RemoveWindow(2)
149 if id, _ := w.Focused(); id != 1 {
150 t.Fatal(id)
151 }
152 w.FocusWindow(1)
153 if id, _ := w.Focused(); id != 3 {
154 t.Fatal(id)
155 }
156 w.FocusWindow(-1)
157 p := w.Layout()
158 if p[0].Rect != (Rect{X: 5, Y: 5, W: 90, H: 33}) || p[1].Rect != (Rect{X: 5, Y: 43, W: 90, H: 33}) || p[0].Neighbors != ports.SideBottom || p[1].Inset != ports.SideAll {
159 t.Fatal(p)
160 }
161 w.ToggleFullscreen()
162 if p = w.Layout(); !p[0].Fullscreen || p[0].Rect != (Rect{X: 0, Y: 0, W: 100, H: 81}) || !p[1].Hidden || p[1].Rect != (Rect{}) {
163 t.Fatal(p)
164 }
165 w.ToggleFullscreen()
166 if w.Layout()[0].Fullscreen {
167 t.Fatal("toggle")
168 }
169 w.SetUsable(Rect{X: 10, Y: 10, W: 60, H: 50})
170 w.SetGaps(2)
171 if w.Layout()[0].Rect.X != 12 {
172 t.Fatal(w.Layout())
173 }
174 w.SetOutput(120, 90)
175 if w.Usable.W != 60 {
176 t.Fatal(w.Usable)
177 }
178}
179func TestMaximizeColumnScroll(t *testing.T) {
180 w := workspace()
181 for id := WindowID(1); id <= 3; id++ {
182 w.AddWindow(id)
183 }
184 w.Columns[1].Cells = 2
185 w.FocusID(2)
186 w.Apply(ActionMaximizeColumn)
187 if !w.Columns[1].FullWidth || w.Columns[1].Cells != 2 || w.Layout()[1].Rect != (Rect{X: 5, Y: 5, W: 90, H: 71}) {
188 t.Fatal(w.Columns, w.Layout())
189 }
190 if w.Layout()[0].Rect.X >= 0 || w.Layout()[2].Rect.X < 100 || w.Layout()[1].Fullscreen {
191 t.Fatal(w.Layout())
192 }
193 w.Apply(ActionMaximizeColumn)
194 if w.Columns[1].FullWidth || w.Columns[1].Cells != 2 || w.Layout()[1].Rect.W != 89 {
195 t.Fatal(w.Columns, w.Layout())
196 }
197}
198
199func TestMaximizeColumnFixed(t *testing.T) {
200 w := workspace()
201 w.Overflow = OverflowFixed
202 for id := WindowID(1); id <= 3; id++ {
203 w.AddWindow(id)
204 }
205 w.FocusID(2)
206 w.Apply(ActionMaximizeColumn)
207 p := w.Layout()
208 if p[1].Rect != (Rect{X: 5, Y: 5, W: 90, H: 71}) || p[1].Fullscreen || !p[0].Hidden || !p[2].Hidden {
209 t.Fatal(p)
210 }
211 w.FocusColumn(1)
212 p = w.Layout()
213 if w.Columns[1].FullWidth || p[2].Hidden || p[0].Hidden || p[1].Hidden {
214 t.Fatal(w.Columns, p)
215 }
216}
217
218func TestMaximizeColumnFixedAddWindow(t *testing.T) {
219 w := workspace()
220 w.Overflow = OverflowFixed
221 w.AddWindow(1)
222 w.AddWindow(2)
223 w.ToggleFullWidth()
224 w.AddWindow(3)
225 p := w.Layout()
226 if w.Columns[1].FullWidth || p[0].Hidden || p[1].Hidden || p[2].Hidden || p[2].Rect.W == 0 {
227 t.Fatal(w.Columns, p)
228 }
229}
230
231func TestCellsAndActions(t *testing.T) {
232 w := workspace()
233 w.AddWindow(1)
234 w.AddWindow(2)
235 // Scroll overflow: auto is 1 cell of 2, then 2 (the whole width), then auto.
236 w.Apply(ActionCycleColumnWidth)
237 if w.Columns[1].Cells != 2 {
238 t.Fatal(w.Columns)
239 }
240 w.GrowColumn(true)
241 if w.Columns[1].Cells != 0 {
242 t.Fatal(w.Columns)
243 }
244 w.RemoveWindow(2)
245 for _, tc := range []struct {
246 a Action
247 want Effect
248 }{{ActionSpawnTerminal, Effect{Spawn: true}}, {ActionCloseWindow, Effect{Close: 1}}, {ActionQuit, Effect{Quit: true}}, {ActionFocusColumnLeft, Effect{}}, {ActionFocusColumnRight, Effect{}}, {ActionFocusWindowUp, Effect{}}, {ActionFocusWindowDown, Effect{}}, {ActionMoveColumnLeft, Effect{}}, {ActionMoveColumnRight, Effect{}}, {ActionToggleFullscreen, Effect{}}, {ActionCycleColumnWidth, Effect{}}, {"spawn fuzzel --prompt x", Effect{Spawn: true, Commands: [][]string{{"fuzzel", "--prompt", "x"}}}}, {"spawn a -x; ; b", Effect{Spawn: true, Commands: [][]string{{"a", "-x"}, {"b"}}}}, {"spawn ", Effect{}}, {"spawn ; ", Effect{}}} {
249 if got := w.Apply(tc.a); !reflect.DeepEqual(got, tc.want) {
250 t.Errorf("%s: %+v", tc.a, got)
251 }
252 }
253}
254func FuzzWorkspaceOps(f *testing.F) {
255 f.Add([]byte{0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
256 f.Fuzz(func(t *testing.T, ops []byte) {
257 w := workspace()
258 next := WindowID(1)
259 for _, op := range ops {
260 switch op % 18 {
261 case 0:
262 w.AddWindow(next)
263 next++
264 case 1:
265 if id, ok := w.Focused(); ok {
266 w.RemoveWindow(id)
267 }
268 case 2:
269 w.FocusColumn(-1)
270 case 3:
271 w.FocusColumn(1)
272 case 4:
273 w.FocusWindow(-1)
274 case 5:
275 w.FocusWindow(1)
276 case 6:
277 w.MoveColumn(-1)
278 case 7:
279 w.MoveColumn(1)
280 case 8:
281 w.GrowColumn(op%2 == 0)
282 case 9:
283 w.ToggleFullscreen()
284 case 10:
285 w.SetGaps(int(op) % 10)
286 case 11:
287 w.SetOutput(int(op)-50, int(op)-80)
288 case 12:
289 w.SetUsable(Rect{X: int(op) % 20, Y: int(op) % 20, W: int(op) - 50, H: int(op) - 80})
290 case 13:
291 w.SetGaps(int(op) * 2)
292 case 14:
293 if len(w.Columns) > 1 {
294 stackFocused(w)
295 }
296 case 15:
297 if len(w.Columns) > 0 {
298 w.Columns[w.Focus].Cells = math.MaxInt
299 w.scroll()
300 }
301 case 16:
302 w.SetOutput(0, 0)
303 case 17:
304 w.SetUsable(Rect{W: math.MaxInt, H: math.MaxInt})
305 }
306 seen := map[WindowID]bool{}
307 for _, c := range w.Columns {
308 if len(c.Windows) == 0 || c.Focus < 0 || c.Focus >= len(c.Windows) {
309 t.Fatal("invalid column")
310 }
311 for _, id := range c.Windows {
312 if seen[id] {
313 t.Fatal("duplicate")
314 }
315 seen[id] = true
316 }
317 }
318 if len(w.Columns) > 0 {
319 if w.Focus < 0 || w.Focus >= len(w.Columns) {
320 t.Fatal("focus")
321 }
322 left := w.columnX(w.Focus) - w.View
323 width := w.columnWidth(w.Focus)
324 minX, maxX := w.Usable.X+w.gap(), w.Usable.X+w.Usable.W-w.gap()
325 if w.fullscreenColumn(w.Focus) {
326 minX, maxX = 0, w.Output.W
327 }
328 if width <= maxX-minX && (left < minX || left+width > maxX) {
329 t.Fatal("scroll")
330 }
331 }
332 placements := w.Layout()
333 if len(placements) != len(seen) {
334 t.Fatal("count")
335 }
336 fullCount := 0
337 for _, p := range placements {
338 if p.Fullscreen {
339 fullCount++
340 }
341 r := p.Rect
342 if r.W < 0 || r.H < 0 || r.Y < 0 || r.Y > w.Output.H || r.H > w.Output.H-r.Y {
343 t.Fatalf("bounds: %+v output %+v", p, w.Output)
344 }
345 if p.Hidden && r != (Rect{}) {
346 t.Fatal("hidden rect")
347 }
348 if !p.Hidden && !p.Fullscreen && (r.Y < w.Usable.Y || r.Y > w.Usable.Y+w.Usable.H || r.H > w.Usable.Y+w.Usable.H-r.Y) {
349 t.Fatalf("usable vertical bounds: %+v usable %+v", p, w.Usable)
350 }
351 if !seen[p.ID] {
352 t.Fatal("missing")
353 }
354 delete(seen, p.ID)
355 }
356 if fullCount > 1 {
357 t.Fatal("multiple fullscreen")
358 }
359 }
360 })
361}
362
363// stackFocused moves the next column's window into the focused column.
364func stackFocused(w *Workspace) {
365 i := w.Focus
366 j := (i + 1) % len(w.Columns)
367 id := w.Columns[j].Windows[0]
368 w.Columns[i].Windows = append(w.Columns[i].Windows, id)
369 w.Columns[j].Windows = w.Columns[j].Windows[1:]
370 if len(w.Columns[j].Windows) == 0 {
371 w.Columns = append(w.Columns[:j], w.Columns[j+1:]...)
372 if j < i {
373 w.Focus--
374 }
375 } else if w.Columns[j].Focus >= len(w.Columns[j].Windows) {
376 w.Columns[j].Focus = len(w.Columns[j].Windows) - 1
377 }
378 w.scroll()
379}
380
381func TestLargeWidthAndBounds(t *testing.T) {
382 w := workspace()
383 w.AddWindow(1)
384 w.SetUsable(Rect{X: -1, Y: -2, W: -10, H: math.MaxInt})
385 w.SetGaps(math.MaxInt)
386 for _, p := range w.Layout() {
387 if p.Rect.W != 0 || p.Rect.H < 0 {
388 t.Fatal(p)
389 }
390 }
391 w.SetOutput(-1, -1)
392 if w.Output.W != 0 || w.Output.H != 0 {
393 t.Fatal(w.Output)
394 }
395}
396func TestUnchangedUsablePreservesView(t *testing.T) {
397 w := workspace()
398 w.AddWindow(1)
399 w.AddWindow(2)
400 w.AddWindow(3)
401 before := w.View
402 if before == 0 {
403 t.Fatal("expected a scrolled workspace")
404 }
405 w.SetUsable(w.Usable)
406 if w.View != before {
407 t.Fatalf("View changed from %d to %d", before, w.View)
408 }
409}
410
411func TestFullscreenPersistsAndScrolls(t *testing.T) {
412 w := workspace()
413 w.AddWindow(1)
414 w.AddWindow(2)
415 w.FocusColumn(-1)
416 w.ToggleFullscreen()
417 if p := w.Layout()[0]; !p.Fullscreen || p.Rect.X != 0 || p.Rect.W != 100 {
418 t.Fatal(p)
419 }
420 w.FocusColumn(1)
421 if p := w.Layout()[0]; !p.Fullscreen || p.Rect.X >= 0 {
422 t.Fatal(p)
423 }
424 w.ToggleFullscreen()
425 if w.Layout()[0].Fullscreen || !w.Layout()[1].Fullscreen {
426 t.Fatal(w.Layout())
427 }
428 w.RemoveWindow(2)
429 if w.Layout()[0].Fullscreen {
430 t.Fatal(w.Layout())
431 }
432}
433
434func TestSetFullscreenStackDeactivation(t *testing.T) {
435 w := workspace()
436 w.Columns = []Column{{Windows: []WindowID{1, 2}, Focus: 0}}
437 // The covering fullscreen window is the only one drawn, so it has the
438 // focus; window 1 gets it back when 2 leaves fullscreen.
439 w.SetFullscreen(2, true)
440 p := w.Layout()
441 if !p[0].Hidden || !p[1].Fullscreen || !p[1].Focused || p[0].Focused {
442 t.Fatal(p)
443 }
444 w.SetFullscreen(2, false)
445 if p := w.Layout(); p[0].Hidden || !p[0].Focused {
446 t.Fatal(p)
447 }
448}
449
450// Fixed overflow keeps every window on screen: past max-columns, new columns
451// split the newest one, alternating top/bottom and left/right.
452func TestFixedOverflowSpiral(t *testing.T) {
453 w := &Workspace{MaxColumns: 2, Overflow: OverflowFixed}
454 w.SetOutput(100, 80)
455 for id := WindowID(1); id <= 5; id++ {
456 w.AddWindow(id)
457 }
458 got := map[WindowID]Rect{}
459 for _, p := range w.Layout() {
460 got[p.ID] = p.Rect
461 }
462 want := map[WindowID]Rect{
463 1: {X: 0, Y: 0, W: 50, H: 80},
464 2: {X: 50, Y: 0, W: 50, H: 40}, // top half of slot 2
465 3: {X: 50, Y: 40, W: 25, H: 40}, // left of the bottom half
466 4: {X: 75, Y: 40, W: 25, H: 20},
467 5: {X: 75, Y: 60, W: 25, H: 20},
468 }
469 if !reflect.DeepEqual(got, want) {
470 t.Fatalf("got %v\nwant %v", got, want)
471 }
472 if w.View != 0 {
473 t.Fatal("fixed overflow scrolled", w.View)
474 }
475 // Up to max-columns it is the plain equal split.
476 w2 := &Workspace{MaxColumns: 2, Overflow: OverflowFixed}
477 w2.SetOutput(100, 80)
478 w2.AddWindow(1)
479 w2.AddWindow(2)
480 if p := w2.Layout(); p[0].Rect.W != 50 || p[1].Rect != (Rect{X: 50, Y: 0, W: 50, H: 80}) {
481 t.Fatal(p)
482 }
483}
484
485// In fixed overflow, cycle-column-width grows the focused column by one
486// cell, taking it from another column of the row.
487func TestFixedGrowColumn(t *testing.T) {
488 rects := func(w *Workspace) map[WindowID]Rect {
489 got := map[WindowID]Rect{}
490 for id, p := range placements(w) {
491 got[id] = p.Rect
492 }
493 return got
494 }
495 check := func(t *testing.T, w *Workspace, want map[WindowID]Rect) {
496 t.Helper()
497 if got := rects(w); !reflect.DeepEqual(got, want) {
498 t.Fatalf("got %v\nwant %v", got, want)
499 }
500 }
501 t.Run("free cell", func(t *testing.T) {
502 w := fixedWorkspace(4, 0, 2)
503 w.FocusID(1)
504 w.GrowColumn(true)
505 check(t, w, map[WindowID]Rect{1: {W: 75, H: 80}, 2: {X: 75, W: 25, H: 80}})
506 w.GrowColumn(true)
507 check(t, w, map[WindowID]Rect{1: {W: 50, H: 80}, 2: {X: 50, W: 50, H: 80}})
508 })
509 t.Run("middle column", func(t *testing.T) {
510 w := fixedWorkspace(4, 0, 3)
511 w.FocusID(2)
512 w.GrowColumn(true)
513 // The remainder cell was the step.
514 check(t, w, map[WindowID]Rect{1: {W: 25, H: 80}, 2: {X: 25, W: 50, H: 80}, 3: {X: 75, W: 25, H: 80}})
515 // No donor is left: back to equal shares.
516 w.GrowColumn(true)
517 check(t, w, map[WindowID]Rect{1: {W: 33, H: 80}, 2: {X: 33, W: 33, H: 80}, 3: {X: 66, W: 34, H: 80}})
518 })
519 t.Run("with gaps", func(t *testing.T) {
520 w := fixedWorkspace(4, 4, 2)
521 w.FocusID(1)
522 w.GrowColumn(true)
523 // Three cells and one of the 88 px left by the three gaps.
524 check(t, w, map[WindowID]Rect{
525 1: {X: 4, Y: 4, W: 66, H: 72},
526 2: {X: 74, Y: 4, W: 22, H: 72},
527 })
528 })
529 t.Run("stays on focus change and new windows", func(t *testing.T) {
530 w := fixedWorkspace(4, 0, 2)
531 w.FocusID(1)
532 w.GrowColumn(true)
533 w.FocusColumn(1)
534 w.AddWindow(3)
535 // 3, 1 and an auto column that weighs one cell.
536 check(t, w, map[WindowID]Rect{1: {W: 60, H: 80}, 2: {X: 60, W: 20, H: 80}, 3: {X: 80, W: 20, H: 80}})
537 })
538 t.Run("the wider column gives a cell to the focused one", func(t *testing.T) {
539 w := fixedWorkspace(4, 0, 2)
540 w.FocusID(1)
541 w.GrowColumn(true)
542 w.FocusID(2)
543 w.GrowColumn(true)
544 check(t, w, map[WindowID]Rect{1: {W: 50, H: 80}, 2: {X: 50, W: 50, H: 80}})
545 })
546 t.Run("closing it shares again", func(t *testing.T) {
547 w := fixedWorkspace(4, 0, 3)
548 w.FocusID(1)
549 w.GrowColumn(true)
550 w.RemoveWindow(1)
551 check(t, w, map[WindowID]Rect{2: {W: 50, H: 80}, 3: {X: 50, W: 50, H: 80}})
552 })
553 t.Run("no-op when it cannot grow", func(t *testing.T) {
554 for _, w := range []*Workspace{fixedWorkspace(3, 0, 1), fixedWorkspace(2, 0, 2), fixedWorkspace(3, 0, 5)} {
555 before := rects(w)
556 w.GrowColumn(true)
557 check(t, w, before)
558 }
559 })
560 t.Run("stays on screen when gaps do not fit", func(t *testing.T) {
561 for _, w := range []*Workspace{fixedWorkspace(4, 4, 2), fixedWorkspace(4, 4, 3)} {
562 w.SetOutput(12, 80)
563 w.FocusID(2)
564 w.GrowColumn(true)
565 for id, p := range placements(w) {
566 r := p.Rect
567 if r.X < 0 || r.Y < 0 || r.W < 0 || r.H < 0 || r.X+r.W > w.Output.W || r.Y+r.H > w.Output.H {
568 t.Fatal(id, r)
569 }
570 }
571 }
572 })
573 t.Run("maximize wins and gives it back", func(t *testing.T) {
574 w := fixedWorkspace(4, 0, 2)
575 w.FocusID(1)
576 w.GrowColumn(true)
577 w.ToggleFullWidth()
578 if p := placements(w); p[1].Rect != (Rect{W: 100, H: 80}) || !p[2].Hidden {
579 t.Fatal(p)
580 }
581 w.ToggleFullWidth()
582 check(t, w, map[WindowID]Rect{1: {W: 75, H: 80}, 2: {X: 75, W: 25, H: 80}})
583 })
584}
585
586// Up and down cross the columns the spiral stacks.
587func TestFixedFocusStackedColumns(t *testing.T) {
588 focused := func(w *Workspace) WindowID { id, _ := w.Focused(); return id }
589 t.Run("spiral", func(t *testing.T) {
590 // 1 left, 2 top right, 3 bottom left, 4 over 5 bottom right.
591 w := fixedWorkspace(2, 4, 5)
592 for _, want := range []WindowID{4, 2} {
593 if !w.FocusWindow(-1) || focused(w) != want {
594 t.Fatal(focused(w), want)
595 }
596 }
597 if w.FocusWindow(-1) {
598 t.Fatal("moved past the top", focused(w))
599 }
600 w.FocusID(1)
601 if w.FocusWindow(1) {
602 t.Fatal("full height column moved", focused(w))
603 }
604 })
605 t.Run("left and right follow the screen", func(t *testing.T) {
606 // 1 left, 2 top right, 3 bottom left, 4 over 5 bottom right.
607 w := fixedWorkspace(2, 4, 5)
608 for _, step := range []struct {
609 dir int
610 want WindowID
611 }{{-1, 3}, {-1, 1}, {1, 2}} {
612 w.FocusColumn(step.dir)
613 if focused(w) != step.want {
614 t.Fatal(focused(w), step.want)
615 }
616 }
617 w.FocusID(3)
618 w.FocusColumn(1)
619 if focused(w) != 4 {
620 t.Fatal(focused(w))
621 }
622 if w.FocusID(2); w.columnToward(1) >= 0 {
623 t.Fatal("right edge has a neighbor")
624 }
625 })
626 t.Run("a row of wide and narrow columns", func(t *testing.T) {
627 // 1 wide; 2, 3 and 4 beside it in one row.
628 w := fixedWorkspace(4, 0, 4)
629 w.Columns[0].Cells, w.Columns[1].Cells, w.Columns[2].Cells, w.Columns[3].Cells = 2, 1, 0, 0
630 w.FocusID(2)
631 if w.FocusWindow(1) || w.FocusWindow(-1) || focused(w) != 2 {
632 t.Fatal("a single row has no window above or below", focused(w))
633 }
634 w.FocusColumn(1)
635 if focused(w) != 3 {
636 t.Fatal(focused(w))
637 }
638 w.FocusColumn(-1)
639 w.FocusColumn(-1)
640 if focused(w) != 1 {
641 t.Fatal(focused(w))
642 }
643 })
644 t.Run("lands on the nearest window of a stacked column", func(t *testing.T) {
645 // 1 left; 2 top right holds two windows; 3 bottom right.
646 w := &Workspace{MaxColumns: 2, Overflow: OverflowFixed}
647 w.SetOutput(100, 80)
648 w.Columns = []Column{{Windows: []WindowID{1}}, {Windows: []WindowID{2, 5}}, {Windows: []WindowID{3}}}
649 w.Focus = 2
650 if !w.FocusWindow(-1) || focused(w) != 5 {
651 t.Fatal(focused(w))
652 }
653 })
654}
655
656func fixedWorkspace(maxCols, gaps, n int) *Workspace {
657 w := &Workspace{MaxColumns: maxCols, Overflow: OverflowFixed, Gaps: gaps}
658 w.SetOutput(100, 80)
659 for id := 1; id <= n; id++ {
660 w.AddWindow(WindowID(id))
661 }
662 return w
663}
664
665func placements(w *Workspace) map[WindowID]Placement {
666 got := map[WindowID]Placement{}
667 for _, p := range w.Layout() {
668 got[p.ID] = p
669 }
670 return got
671}
672
673func TestFixedOverflowEdgeCases(t *testing.T) {
674 t.Run("new windows go last whatever the focus", func(t *testing.T) {
675 w := fixedWorkspace(2, 0, 3)
676 w.FocusID(1)
677 w.AddWindow(4)
678 if got := w.Columns[len(w.Columns)-1].Windows[0]; got != 4 {
679 t.Fatal(got)
680 }
681 })
682 for _, col := range []WindowID{1, 4} { // a plain column and a spiral slot
683 t.Run("fullscreen covers the output", func(t *testing.T) {
684 w := fixedWorkspace(2, 4, 4)
685 w.FocusID(col)
686 w.ToggleFullscreen()
687 for id, p := range placements(w) {
688 if id == col && (p.Rect != Rect{W: 100, H: 80} || !p.Fullscreen) {
689 t.Fatal(p)
690 }
691 if id != col && !p.Hidden {
692 t.Fatalf("window %d visible over fullscreen: %+v", id, p)
693 }
694 }
695 })
696 }
697 t.Run("a full row does not grow", func(t *testing.T) {
698 w := fixedWorkspace(2, 0, 2)
699 w.GrowColumn(true)
700 w.FocusColumn(-1)
701 w.GrowColumn(true)
702 for id, p := range placements(w) {
703 if p.Rect.X+p.Rect.W > 100 || p.Rect.W != 50 {
704 t.Fatal(id, p)
705 }
706 }
707 })
708 t.Run("gaps between spiral halves", func(t *testing.T) {
709 got := placements(fixedWorkspace(1, 4, 2))
710 if got[1].Rect != (Rect{X: 4, Y: 4, W: 92, H: 34}) || got[2].Rect != (Rect{X: 4, Y: 42, W: 92, H: 34}) {
711 t.Fatal(got[1].Rect, got[2].Rect)
712 }
713 if got[1].Neighbors == 0 || got[2].Neighbors == 0 {
714 t.Fatal("spiral windows must keep borders to show focus")
715 }
716 })
717}
718
719// Activating a window hidden by another's fullscreen shows it: the
720// fullscreen is exclusive, so it is left, even for a float.
721func TestActivateLeavesFullscreen(t *testing.T) {
722 for _, tc := range []struct {
723 name string
724 setup func(w *Workspace)
725 keep bool
726 }{
727 {"fixed", func(w *Workspace) { w.Overflow = OverflowFixed; w.MaxColumns = 2; w.AddWindow(3) }, false},
728 {"same column", func(w *Workspace) { w.Columns[0].Windows = append(w.Columns[0].Windows, 3) }, false},
729 {"floating", func(w *Workspace) { w.AddFloating(3, 10, 10) }, false},
730 } {
731 t.Run(tc.name, func(t *testing.T) {
732 w := &Workspace{}
733 w.SetOutput(100, 80)
734 w.AddWindow(2)
735 tc.setup(w)
736 w.FocusID(2)
737 w.SetFullscreen(2, true)
738 w.Activate(3)
739 for _, p := range w.Layout() {
740 if p.ID == 3 && (p.Hidden || !p.Focused) {
741 t.Fatalf("3 not shown and focused: %+v", p)
742 }
743 if p.ID == 2 && p.Fullscreen != tc.keep {
744 t.Fatalf("fullscreen of 2 = %v", p.Fullscreen)
745 }
746 }
747 })
748 }
749}