1package core
2
3import (
4 "slices"
5 "testing"
6 "time"
7)
8
9func TestBorderAt(t *testing.T) {
10 for _, mode := range []Overflow{OverflowFixed, OverflowCascade, OverflowScroll} {
11 for _, gap := range []int{0, 10} {
12 w := spanWorkspace(mode, 2)
13 w.SetGaps(gap)
14 w.View = 0
15 w.view = slide{}
16 rs := w.columnRects()
17 x := rs[0].X + rs[0].W + gap/2
18 y := rs[0].Y + 100
19 b, ok := w.borderAt(x, y, 6)
20 if !ok || b.kind != borderColumn || b.col != 0 {
21 t.Fatal(mode, gap, b, ok)
22 }
23 if _, ok = w.borderAt(x-5, y, 6); !ok {
24 t.Fatal("padding")
25 }
26 if _, ok = w.borderAt(rs[0].X+20, y, 6); ok {
27 t.Fatal("tile interior")
28 }
29 w.Columns[0].Windows = append(w.Columns[0].Windows, 9)
30 rows := rowRects(rs[0], w.Columns[0], gap)
31 b, ok = w.borderAt(rs[0].X+20, rows[0].Y+rows[0].H+gap/2, 6)
32 if !ok || b.kind != borderRow {
33 t.Fatal("row", b, ok)
34 }
35 w.AddFloating(10, 400, 400)
36 f := w.floatRect(w.Floats[0])
37 if _, ok = w.borderAt(f.X+f.W/2, f.Y+f.H/2, 6); ok {
38 t.Fatal("float")
39 }
40 w.FocusID(1)
41 w.SetFullscreen(1, true)
42 if _, ok = w.borderAt(x, y, 6); ok {
43 t.Fatal("fullscreen")
44 }
45 }
46 }
47 w := spanWorkspace(OverflowFixed, 4)
48 rs := w.columnRects()
49 if _, ok := w.borderAt(rs[0].X+rs[0].W+5, 100, 8); ok {
50 t.Fatal("spiral")
51 }
52 w = spanWorkspace(OverflowFixed, 2)
53 w.Columns[w.Focus].FullWidth = true
54 if _, ok := w.borderAt(500, 100, 8); ok {
55 t.Fatal("maximized")
56 }
57 w = spanWorkspace(OverflowCascade, 4)
58 rs = w.columnRects()
59 if _, ok := w.borderAt(rs[2].X+rs[2].W+5, 100, 8); ok {
60 t.Fatal("different bands")
61 }
62 // Presentation offsets must be applied just like Layout.
63 w = spanWorkspace(OverflowScroll, 4)
64 w.View = 100
65 rs = w.columnRects()
66 b, ok := w.borderAt(rs[0].X+rs[0].W+5, 100, 6)
67 if !ok || b.col != 0 {
68 t.Fatal("view offset", b, ok)
69 }
70}
71
72func TestSetColumnBorder(t *testing.T) {
73 for _, mode := range []Overflow{OverflowFixed, OverflowCascade} {
74 for _, n := range []int{2, 3} {
75 w := spanWorkspace(mode, n)
76 before := w.columnRects()
77 b := border{kind: borderColumn, col: 0}
78 pos := before[0].X + before[0].W + 5 + 50
79 w.setBorder(b, pos)
80 after := w.columnRects()
81 if after[0].X+after[0].W+5 != pos {
82 t.Fatal("position", mode, n, pos, after)
83 }
84 if n == 3 && after[2] != before[2] {
85 t.Fatal("third moved", before, after)
86 }
87 if after[n-1].X+after[n-1].W != 990 {
88 t.Fatal("not pixel filled", after)
89 }
90 total := 0
91 for _, c := range w.Columns {
92 total += c.Span
93 }
94 if total != 10000 {
95 t.Fatal(total)
96 }
97 pair := w.Columns[0].Span + w.Columns[1].Span
98 w.setBorder(b, -1000)
99 after = w.columnRects()
100 if after[0].W < 48 || w.Columns[0].Span+w.Columns[1].Span != pair {
101 t.Fatal("minimum", after)
102 }
103 w.setBorder(b, 10000)
104 after = w.columnRects()
105 if after[1].W < 48 {
106 t.Fatal("right minimum", after)
107 }
108 w.resetBorder(b)
109 for _, c := range w.Columns {
110 if c.Span != 0 || c.spanStamp != 0 {
111 t.Fatal("reset")
112 }
113 }
114 }
115 }
116}
117
118func TestSetBorderCascadeBand(t *testing.T) {
119 w := spanWorkspace(OverflowCascade, 5)
120 before := w.columnRects()
121 w.setBorder(border{kind: borderColumn, col: 0}, 400)
122 after := w.columnRects()
123 if !slices.Equal(before[3:], after[3:]) || w.Columns[3].Span != 0 {
124 t.Fatal("other band changed", before, after)
125 }
126}
127
128func TestSetBorderScroll(t *testing.T) {
129 w := spanWorkspace(OverflowScroll, 3)
130 before := w.columnRects()
131 b := border{kind: borderColumn, col: 0}
132 pos := before[0].X + before[0].W + 5 + 60
133 w.setBorder(b, pos)
134 after := w.columnRects()
135 if after[0].X+after[0].W+5 != pos {
136 t.Fatal("position", after)
137 }
138 for i := 1; i < len(after); i++ {
139 if after[i].W != before[i].W || after[i].X != before[i].X+60 {
140 t.Fatal("right columns", before, after)
141 }
142 }
143 saved := w.Columns[1].Span
144 w.setBorder(b, 10000)
145 if w.columnRects()[0].W != 980 || w.Columns[1].Span != saved {
146 t.Fatal("maximum")
147 }
148 w.setBorder(b, -10000)
149 if w.columnRects()[0].W < 49 {
150 t.Fatal("minimum")
151 }
152}
153
154func TestSetRowBorder(t *testing.T) {
155 w := spanWorkspace(OverflowFixed, 2)
156 w.Columns[0].Windows = []WindowID{1, 3, 4}
157 r := w.columnRects()[0]
158 before := rowRects(r, w.Columns[0], w.gap())
159 b := border{kind: borderRow, col: 0, row: 0}
160 pos := before[0].Y + before[0].H + 5 + 30
161 w.setBorder(b, pos)
162 after := rowRects(r, w.Columns[0], w.gap())
163 if after[0].Y+after[0].H+5 != pos || after[2] != before[2] || sum(w.Columns[0].Shares) != 10000 {
164 t.Fatal(before, after, w.Columns[0].Shares)
165 }
166 pair := w.Columns[0].Shares[0] + w.Columns[0].Shares[1]
167 w.setBorder(b, -10000)
168 if w.Columns[0].Shares[0] != minShare {
169 t.Fatal("minimum")
170 }
171 w.setBorder(b, 10000)
172 if w.Columns[0].Shares[1] != minShare || w.Columns[0].Shares[0]+w.Columns[0].Shares[1] != pair {
173 t.Fatal("pair")
174 }
175 w.resetBorder(b)
176 if w.Columns[0].Shares != nil {
177 t.Fatal("reset")
178 }
179}
180
181func TestBorderPixelRemainders(t *testing.T) {
182 for _, mode := range []Overflow{OverflowFixed, OverflowCascade} {
183 for n := 2; n <= 4; n++ {
184 for width := 997; width <= 1003; width++ {
185 for pair := 0; pair < n-1; pair++ {
186 w := spanWorkspace(mode, n)
187 w.SetMaxColumns(n)
188 for i := range w.Columns {
189 w.Columns[i].BandStart = false
190 }
191 w.SetOutput(width, 800)
192 before := w.columnRects()
193 b := border{kind: borderColumn, col: pair}
194 pos := before[pair].X + before[pair].W + 5
195 w.setBorder(b, pos)
196 if after := w.columnRects(); !slices.Equal(before, after) {
197 t.Fatalf("stamp changed geometry: mode=%v n=%d width=%d pair=%d before=%v after=%v", mode, n, width, pair, before, after)
198 }
199 for delta := -20; delta <= 20; delta++ {
200 w.setBorder(b, pos+delta)
201 after := w.columnRects()
202 for i := range before {
203 if i != pair && i != pair+1 && after[i] != before[i] {
204 t.Fatalf("untouched rect changed: mode=%v n=%d width=%d pair=%d delta=%d before=%v after=%v", mode, n, width, pair, delta, before, after)
205 }
206 }
207 if after[pair].X+after[pair].W+5 != pos+delta {
208 t.Fatal("border missed pointer", before, after)
209 }
210 }
211 }
212 }
213 }
214 }
215}
216
217func TestBorderStampIgnoresMaximizedWidth(t *testing.T) {
218 for _, mode := range []Overflow{OverflowScroll, OverflowFixed, OverflowCascade} {
219 t.Run(string(mode), func(t *testing.T) {
220 w := spanWorkspace(mode, 3)
221 normal := w.columnWidthFor(2, true)
222 w.Columns[2].FullWidth = true
223 rs := w.columnRects()
224 w.setBorder(border{kind: borderColumn, col: 0}, rs[0].X+rs[0].W+15)
225 w.Columns[2].FullWidth = false
226 if got := w.columnWidth(2); got != normal {
227 t.Fatalf("unmaximized width = %d, want %d", got, normal)
228 }
229 })
230 }
231}
232
233func TestSetBorderReportsChange(t *testing.T) {
234 for _, mode := range []Overflow{OverflowScroll, OverflowFixed} {
235 w := spanWorkspace(mode, 2)
236 b := border{kind: borderColumn, col: 0}
237 x := w.columnRects()[0]
238 pos := x.X + x.W + 5 + 40
239 if !w.setBorder(b, pos) {
240 t.Fatal(mode, "first move must report a change")
241 }
242 if w.setBorder(b, pos) {
243 t.Fatal(mode, "same position reported a change")
244 }
245 w.setBorder(b, -10000)
246 if w.setBorder(b, -20000) {
247 t.Fatal(mode, "clamped at the minimum reported a change")
248 }
249 }
250 w := spanWorkspace(OverflowFixed, 2)
251 w.Columns[0].Windows = []WindowID{1, 3}
252 b := border{kind: borderRow, col: 0}
253 r := w.columnRects()[0]
254 pos := rowRects(r, w.Columns[0], w.gap())[0].Y + 200
255 if !w.setBorder(b, pos) || w.setBorder(b, pos) {
256 t.Fatal("row border change reporting")
257 }
258 if w.setBorder(border{kind: borderColumn, col: 9}, 0) {
259 t.Fatal("invalid border reported a change")
260 }
261}
262
263func TestBorderMotionPublishesOnlyChanges(t *testing.T) {
264 c, w := borderCore(t, false)
265 grabButton(t, c, btnRight, true, time.Second)
266 grabCommands(c)
267 grabMove(t, c, c.cursorX+150, c.cursorY)
268 if len(grabCommands(c)) == 0 {
269 t.Fatal("a resizing motion must publish")
270 }
271 spans := []int{w.Columns[0].Span, w.Columns[1].Span}
272 grabMove(t, c, c.cursorX, c.cursorY)
273 if cmds := grabCommands(c); len(cmds) != 0 {
274 t.Fatalf("unchanged motion published %+v", cmds)
275 }
276 if spans[0] != w.Columns[0].Span || spans[1] != w.Columns[1].Span {
277 t.Fatal("spans changed")
278 }
279 grabMove(t, c, -10000, c.cursorY)
280 grabCommands(c)
281 grabMove(t, c, -20000, c.cursorY)
282 if cmds := grabCommands(c); len(cmds) != 0 {
283 t.Fatalf("motion clamped at the minimum published %+v", cmds)
284 }
285}
286
287func TestSetBorderUndersizedPairReportsStampOnce(t *testing.T) {
288 t.Run("row", func(t *testing.T) {
289 w := spanWorkspace(OverflowFixed, 1)
290 w.Columns[0].Windows = []WindowID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
291 // Ten rows is the most that fit; pixel rounding leaves the last pair
292 // just under twice the minimum share.
293 b := border{kind: borderRow, col: 0, row: 8}
294 rows := rowRects(w.columnRects()[0], w.Columns[0], w.gap())
295 pos := rows[8].Y + 1
296 if !w.setBorder(b, pos) {
297 t.Fatal("stamping an undersized row pair must report a change")
298 }
299 if c := w.Columns[0]; c.Shares[8]+c.Shares[9] >= 2*minShare {
300 t.Fatal("row pair not undersized", c.Shares)
301 }
302 if w.setBorder(b, pos) {
303 t.Fatal("repeat on an undersized row pair reported a change")
304 }
305 })
306 for _, mode := range []Overflow{OverflowFixed, OverflowCascade} {
307 t.Run(string(mode), func(t *testing.T) {
308 w := spanWorkspace(mode, 3)
309 w.border = 300
310 b := border{kind: borderColumn, col: 0}
311 pos := w.columnRects()[0].X + 100
312 if !w.setBorder(b, pos) {
313 t.Fatal("stamping an undersized column pair must report a change")
314 }
315 avail := w.Usable.W - w.gap()*4
316 if pair := w.Columns[0].Span + w.Columns[1].Span; pair >= 2*pixelShare(4*w.border, avail) {
317 t.Fatal("column pair not undersized", pair)
318 }
319 if w.setBorder(b, pos) {
320 t.Fatal("repeat on an undersized column pair reported a change")
321 }
322 })
323 }
324}