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