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, whole(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, whole(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, whole(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, whole(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, whole(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.View == 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, whole(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// A scroll column scrolled under a side panel is no neighbor: the column
151// next to it in the usable area gets no line on that side.
152// With a gap, the edge the two share lies inside the usable area: only
153// the tile under the panel being out of it keeps the line away.
154func TestSeparatorsScrollUnderSidePanel(t *testing.T) {
155 for _, gap := range []int{0, 8} {
156 ps := []Placement{
157 {ID: 1, Rect: Rect{X: 0, W: 100, H: 600}},
158 {ID: 2, Rect: Rect{X: 100 + gap, W: 400, H: 600}, Focused: true},
159 {ID: 3, Rect: Rect{X: 500 + 2*gap, W: 380, H: 600}},
160 }
161 setVisibleNeighbors(ps, gap, Rect{X: 100, W: 900, H: 600})
162 if ps[0].Neighbors != 0 {
163 t.Fatalf("gap %d: tile under the panel: neighbors %04b, want none", gap, ps[0].Neighbors)
164 }
165 if ps[1].Neighbors != ports.SideRight {
166 t.Fatalf("gap %d: tile at the panel: neighbors %04b, want right only", gap, ps[1].Neighbors)
167 }
168 }
169}
170
171// A cascade band above a bottom panel: the next band starts under the
172// panel, so the focused tile gets no line along the panel, only the one it
173// shares with its band neighbor.
174func TestSeparatorsCascadeBottomPanel(t *testing.T) {
175 w := &Workspace{Overflow: OverflowCascade, MaxColumns: 2}
176 w.SetOutput(900, 600)
177 w.SetUsable(Rect{W: 900, H: 580})
178 for id := WindowID(1); id <= 3; id++ {
179 w.AddWindow(id)
180 }
181 w.FocusID(1)
182 ps := w.Layout()
183 if p := previewOf(t, ps, 1); p.Neighbors != ports.SideRight {
184 t.Fatalf("neighbors %04b, want right only", p.Neighbors)
185 }
186 seps := separators(ps, 2, 0, viewport{frame: w.Output, usable: w.Usable}, true)
187 if got := colorAt(seps, 200, 578); got != "" {
188 t.Fatalf("line along the panel: %v", seps)
189 }
190 for _, s := range seps {
191 if s.Window == 3 {
192 t.Fatalf("line of the tile under the panel: %v", seps)
193 }
194 }
195 // Two tiles on screen: each lights its half of the shared line.
196 if got := colorAt(seps, 449, 100); got != "lit" {
197 t.Fatalf("shared line (449,100) = %q; %v", got, seps)
198 }
199 if got := colorAt(seps, 449, 400); got != "gray" {
200 t.Fatalf("shared line (449,400) = %q; %v", got, seps)
201 }
202}
203
204// With gaps each tile has its own line: the focused one lights it whole.
205func TestSeparatorsGaps(t *testing.T) {
206 ps := []Placement{{ID: 1, Rect: Rect{X: 0, W: 16, H: 16}, Focused: true}, {ID: 2, Rect: Rect{X: 20, W: 16, H: 16}}}
207 setVisibleNeighbors(ps, 4, bigOutput)
208 seps := separators(ps, 2, 4, whole(bigOutput), true)
209 for _, c := range []px{{14, 2, "lit"}, {14, 13, "lit"}, {20, 8, "gray"}, {17, 8, ""}} {
210 if got := colorAt(seps, c.x, c.y); got != c.want {
211 t.Errorf("(%d,%d) = %q, want %q", c.x, c.y, got, c.want)
212 }
213 }
214}