1package core
2
3import (
4 "reflect"
5 "testing"
6
7 "github.com/bnema/neferwl/internal/ports"
8)
9
10// tiles lays out rects edge to edge (no gap) with focus on index f.
11func tiles(f int, rs ...Rect) []Placement {
12 ps := make([]Placement, len(rs))
13 for i, r := range rs {
14 ps[i] = Placement{ID: WindowID(i + 1), Rect: r, Focused: i == f}
15 }
16 setVisibleNeighbors(ps, 0, bigOutput)
17 return ps
18}
19
20// bigOutput is an output wider than every test layout.
21var bigOutput = Rect{W: 1000, H: 1000}
22
23// colorAt is what the renderer shows at (x, y): the last separator over
24// it wins. "" is no line.
25func colorAt(seps []ports.Separator, x, y int) string {
26 got := ""
27 for _, s := range seps {
28 r := s.Rect
29 if x >= r.X && x < r.X+r.W && y >= r.Y && y < r.Y+r.H {
30 got = "gray"
31 if s.Active {
32 got = "lit"
33 }
34 }
35 }
36 return got
37}
38
39type px struct {
40 x, y int
41 want string
42}
43
44func TestSeparators(t *testing.T) {
45 cols2 := []Rect{{X: 0, Y: 0, W: 16, H: 16}, {X: 16, Y: 0, W: 16, H: 16}}
46 cols3 := []Rect{{X: 0, Y: 0, W: 16, H: 16}, {X: 16, Y: 0, W: 16, H: 16}, {X: 32, Y: 0, W: 16, H: 16}}
47 // A full-height column and a column split in two.
48 grid := []Rect{{X: 0, Y: 0, W: 16, H: 32}, {X: 16, Y: 0, W: 16, H: 16}, {X: 16, Y: 16, W: 16, H: 16}}
49 for _, tc := range []struct {
50 name string
51 ps []Placement
52 lit bool
53 check []px
54 }{
55 // The left window owns the line (x 14-15); the client of the right
56 // window is not covered.
57 {"two columns, left focused: top half", tiles(0, cols2...), true, []px{{14, 3, "lit"}, {15, 7, "lit"}, {14, 8, "gray"}, {14, 15, "gray"}, {16, 3, ""}, {13, 3, ""}}},
58 {"two columns, right focused: bottom half", tiles(1, cols2...), true, []px{{14, 3, "gray"}, {14, 12, "lit"}}},
59 {"unfocused output: all gray", tiles(0, cols2...), false, []px{{14, 3, "gray"}, {14, 12, "gray"}}},
60 {"three columns, middle: both lines whole", tiles(1, cols3...), true, []px{{14, 3, "lit"}, {14, 12, "lit"}, {30, 3, "lit"}, {30, 12, "lit"}}},
61 {"three columns, first: its line only", tiles(0, cols3...), true, []px{{14, 3, "lit"}, {14, 12, "lit"}, {30, 3, "gray"}, {30, 12, "gray"}}},
62 {"no line at the screen edge", tiles(2, cols3...), true, []px{{47, 3, ""}, {0, 3, ""}}},
63 // The left and top lines meet: the corner square is lit.
64 {"bottom-right tile: corner closed", tiles(2, grid...), true, []px{{14, 30, "lit"}, {14, 20, "lit"}, {14, 14, "lit"}, {14, 15, "lit"}, {14, 5, "gray"}, {20, 14, "lit"}, {31, 15, "lit"}}},
65 {"top-right tile", tiles(1, grid...), true, []px{{14, 3, "lit"}, {14, 15, "lit"}, {14, 20, "gray"}, {20, 14, "lit"}}},
66 {"left column: whole line, split column untouched", tiles(0, grid...), true, []px{{14, 3, "lit"}, {14, 30, "lit"}, {20, 14, "gray"}}},
67 } {
68 t.Run(tc.name, func(t *testing.T) {
69 seps := separators(tc.ps, 2, 0, bigOutput, tc.lit)
70 for _, c := range tc.check {
71 if got := colorAt(seps, c.x, c.y); got != c.want {
72 t.Errorf("(%d,%d) = %q, want %q; %v", c.x, c.y, got, c.want, seps)
73 }
74 }
75 })
76 }
77}
78
79func TestSeparatorsSkip(t *testing.T) {
80 if s := separators(tiles(0, Rect{W: 32, H: 16}), 2, 0, bigOutput, true); len(s) != 0 {
81 t.Fatal("lone window:", s)
82 }
83 if s := separators(tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}), 0, 0, bigOutput, true); len(s) != 0 {
84 t.Fatal("width 0:", s)
85 }
86 full := tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16})
87 full[0].Fullscreen, full[1].Hidden = true, true
88 if s := separators(full, 2, 0, bigOutput, true); len(s) != 0 {
89 t.Fatal("fullscreen:", s)
90 }
91}
92
93func TestSeparatorsFloat(t *testing.T) {
94 float := Placement{ID: 1, Rect: Rect{X: 10, Y: 10, W: 20, H: 20}, Floating: true, Focused: true, Inset: ports.SideAll}
95 seps := separators([]Placement{float}, 2, 0, bigOutput, true)
96 if seps[0].Window != 1 {
97 t.Fatal("float border not tied to its window:", seps)
98 }
99 for _, c := range [][2]int{{10, 10}, {29, 29}, {20, 11}} {
100 if got := colorAt(seps, c[0], c[1]); got != "lit" {
101 t.Errorf("(%d,%d) = %q", c[0], c[1], got)
102 }
103 }
104 if got := colorAt(seps, 20, 20); got != "" {
105 t.Error("inside:", got)
106 }
107}
108
109// Through the real layout: scrolling removes borders to off-output
110// neighbors and gives the client back the reserved space.
111func TestSeparatorsScroll(t *testing.T) {
112 m := monitor() // 100x80, 2 columns on screen
113 for id := WindowID(1); id <= 3; id++ {
114 m.AddWindow(id)
115 }
116 w := m.Current()
117 insets := func() map[WindowID]ports.Sides {
118 got := map[WindowID]ports.Sides{}
119 for _, p := range w.Layout() {
120 got[p.ID] = p.Inset
121 }
122 return got
123 }
124 before := insets()
125 if before[1] != 0 || before[3]&ports.SideRight != 0 {
126 t.Fatalf("scrolled tiles reserve off-output lines: %v", before)
127 }
128 if w.ViewX == 0 {
129 t.Fatal("expected a scrolled view")
130 }
131 m.Apply(ActionFocusColumnLeft)
132 m.Apply(ActionFocusColumnLeft)
133 if after := insets(); reflect.DeepEqual(before, after) || after[1]&ports.SideRight == 0 || after[3] != 0 {
134 t.Fatalf("expected visible borders to change with the view: %v then %v", before, after)
135 }
136 out := Rect{W: 100, H: 80}
137 seps := separators(w.Layout(), 2, 0, out, true)
138 if got := colorAt(seps, 99, 40); got != "" {
139 t.Fatalf("line at the output edge: %v", seps)
140 }
141 // Two columns on screen out of three: the half rule applies.
142 if got := colorAt(seps, 48, 60); got != "gray" {
143 t.Fatalf("(48,60) = %q; %v", got, seps)
144 }
145 if got := colorAt(seps, 48, 10); got != "lit" {
146 t.Fatalf("(48,10) = %q; %v", got, seps)
147 }
148}
149
150// With gaps each tile has its own line: the focused one lights it whole.
151func TestSeparatorsGaps(t *testing.T) {
152 ps := []Placement{{ID: 1, Rect: Rect{X: 0, W: 16, H: 16}, Focused: true}, {ID: 2, Rect: Rect{X: 20, W: 16, H: 16}}}
153 setVisibleNeighbors(ps, 4, bigOutput)
154 seps := separators(ps, 2, 4, bigOutput, true)
155 for _, c := range []px{{14, 2, "lit"}, {14, 13, "lit"}, {20, 8, "gray"}, {17, 8, ""}} {
156 if got := colorAt(seps, c.x, c.y); got != c.want {
157 t.Errorf("(%d,%d) = %q, want %q", c.x, c.y, got, c.want)
158 }
159 }
160}