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