1package core_test
2
3import (
4 "testing"
5 "time"
6
7 "github.com/bnema/neferwl/internal/core"
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11// xOf is the X of window id in s, shown or not.
12func xOf(t *testing.T, s ports.Scene, id ports.WindowID) int {
13 t.Helper()
14 for _, w := range s.Windows {
15 if w.ID == id {
16 return w.Rect.X
17 }
18 }
19 t.Fatalf("window %d not in the scene", id)
20 return 0
21}
22
23// fourColumns maps four windows of half the width and goes back to the
24// first column, settled: the view is at 0.
25func fourColumns(t *testing.T, r *swipeRig) {
26 t.Helper()
27 for id := ports.WindowID(1); id <= 4; id++ {
28 r.mapWindow(t, id)
29 }
30 for range 3 {
31 r.key(t, "Left", ports.ModAlt)
32 }
33 if r.cfg.Animations.On {
34 r.settle(t)
35 }
36}
37
38// A key that scrolls the view shows the old view right after it, an
39// intermediate one on the first frame and the final one once settled.
40func TestKeyScrollTransition(t *testing.T) {
41 r := startSwipe(t, nil)
42 threeColumns(t, r)
43 r.key(t, "Left", ports.ModAlt)
44 r.key(t, "Left", ports.ModAlt)
45 r.settle(t)
46 r.key(t, "Right", ports.ModAlt) // 2 is on screen: nothing scrolls
47 s := r.key(t, "Right", ports.ModAlt)[0]
48 if got := xOf(t, s, 1); got != 0 {
49 t.Fatalf("column 1 at %d right after the key, want the old 0", got)
50 }
51 got := xOf(t, r.frame(t, 16*time.Millisecond), 1)
52 if got >= 0 || got <= -400 {
53 t.Fatalf("column 1 at %d on the first frame, want between 0 and -400", got)
54 }
55 if got := xOf(t, r.settle(t), 1); got != -400 {
56 t.Fatalf("column 1 at %d settled, want -400", got)
57 }
58}
59
60// Presses during a transition never send the view backward.
61func TestChainedKeysNeverJumpBack(t *testing.T) {
62 r := startSwipe(t, nil)
63 fourColumns(t, r)
64 r.key(t, "Right", ports.ModAlt)
65 r.key(t, "Right", ports.ModAlt)
66 prev := xOf(t, r.frame(t, 16*time.Millisecond), 1)
67 r.key(t, "Right", ports.ModAlt)
68 for i := range 40 {
69 got := xOf(t, r.frame(t, 16*time.Millisecond), 1)
70 if got > prev {
71 t.Fatalf("frame %d: column 1 at %d after %d, the view went back", i, got, prev)
72 }
73 prev = got
74 if got == -800 {
75 return
76 }
77 }
78 t.Fatalf("column 1 settled at %d, want -800", prev)
79}
80
81func TestWorkspaceKeyTransition(t *testing.T) {
82 r := startSwipe(t, nil)
83 r.workspaces(t)
84 s := r.key(t, "Next", ports.ModAlt)[0]
85 if got, ok := rectOf(s, 1); !ok || got.Y != 0 {
86 t.Fatalf("workspace 1 at %+v (shown %t) right after the key, want the old position", got, ok)
87 }
88 s = r.frame(t, 16*time.Millisecond)
89 got, ok := rectOf(s, 2)
90 if !ok || got.Y <= 0 || got.Y >= 600 {
91 t.Fatalf("workspace 2 at %+v (shown %t) on the first frame, want Y between 0 and 600", got, ok)
92 }
93 if got, _ := rectOf(r.settle(t), 2); got.Y != 0 {
94 t.Fatalf("workspace 2 at %d settled, want 0", got.Y)
95 }
96}
97
98// The workspace slide of a key keeps running when the next key goes on.
99func TestChainedWorkspaceKeysKeepSliding(t *testing.T) {
100 r := startSwipe(t, nil)
101 r.workspaces(t)
102 r.keySettled(t, "Next")
103 r.keySettled(t, "Next")
104 r.mapWindow(t, 3)
105 r.keySettled(t, "Prior")
106 r.keySettled(t, "Prior")
107 r.key(t, "Next", ports.ModAlt)
108 r.frame(t, 16*time.Millisecond)
109 r.key(t, "Next", ports.ModAlt)
110 if got, ok := rectOf(r.settle(t), 3); !ok || got.Y != 0 {
111 t.Fatalf("workspace 3 at %+v (shown %t) settled", got, ok)
112 }
113}
114
115func TestAnimationsOffKeysLandAtOnce(t *testing.T) {
116 r := startSwipe(t, animationsOff)
117 fourColumns(t, r)
118 r.key(t, "Right", ports.ModAlt)
119 if got := xOf(t, r.key(t, "Right", ports.ModAlt)[0], 1); got != -400 {
120 t.Fatalf("column 1 at %d right after the key, want -400", got)
121 }
122 r.noFrameScene(t)
123
124 r = startSwipe(t, animationsOff)
125 r.workspaces(t)
126 s := r.key(t, "Next", ports.ModAlt)[0]
127 if got, ok := rectOf(s, 2); !ok || got.Y != 0 {
128 t.Fatalf("workspace 2 at %+v (shown %t) right after the key, want the settled rect", got, ok)
129 }
130 r.noFrameScene(t)
131}
132
133// The overview has no camera: opening it on a scrolled view, a bind that
134// moves the selection and closing it only move the cards (rect motions), which
135// settle: no spring runs afterwards and nothing slides the view.
136func TestNoTransitionWithOverview(t *testing.T) {
137 for _, tc := range []struct {
138 name string
139 // open is what the overview shows before the keys of the bind.
140 sym string
141 mods ports.Mods
142 }{
143 {"open and close", "", 0},
144 {"focus bind while open", "Left", ports.ModAlt},
145 {"workspace bind while open", "Next", ports.ModAlt},
146 } {
147 t.Run(tc.name, func(t *testing.T) {
148 r := startSwipe(t, nil)
149 fourColumns(t, r)
150 r.key(t, "Right", ports.ModAlt) // 2 is on screen: no scroll
151 r.keySettled(t, "Right") // the view scrolls away from 0
152 r.keySettled(t, "Right")
153 // settleAll: a bind that moves nothing starts no spring.
154 r.key(t, "o", ports.ModAlt)
155 r.settleAll(t, wide.Name)
156 r.noFrameScene(t)
157 if tc.sym != "" {
158 r.key(t, tc.sym, tc.mods)
159 r.settleAll(t, wide.Name)
160 r.noFrameScene(t)
161 }
162 r.key(t, "o", ports.ModAlt)
163 r.settleAll(t, wide.Name)
164 r.noFrameScene(t)
165 })
166 }
167}
168
169// movedRig plugs a second 800x600 output right of the first.
170func movedRig(t *testing.T, edit func(*ports.Config)) (*swipeRig, ports.OutputInfo) {
171 t.Helper()
172 other := ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000}
173 return startSwipeOn(t, edit, wide, other), other
174}
175
176func outScene(t *testing.T, set []ports.Scene, output string) ports.Scene {
177 t.Helper()
178 for _, s := range set {
179 if s.Output == output {
180 return s
181 }
182 }
183 t.Fatalf("no scene for %s", output)
184 return ports.Scene{}
185}
186
187// A workspace that goes to another monitor slides in on it from where the
188// source drew it, in the destination's coordinates, and settles.
189func TestMovedWorkspaceSlidesInOnDestination(t *testing.T) {
190 r, other := movedRig(t, nil)
191 twoColumns(t, r)
192 set := r.key(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift)
193 dst := outScene(t, set, other.Name)
194 // DP-2 starts at x=800: the source's 0 and 400 are -800 and -400 there.
195 if got1, got2 := rectNow(t, dst, 1), rectNow(t, dst, 2); got1.X != -800 || got2.X != -400 {
196 t.Fatalf("right after the key 1 at %+v and 2 at %+v, want the source's rects (-800, -400)", got1, got2)
197 }
198 set, ok := r.clk.flip(t, r.frames, r.scenes, 16*time.Millisecond, wide.Name, other.Name)
199 if !ok {
200 t.Fatal("no frame while the windows slide")
201 }
202 dst = outScene(t, set, other.Name)
203 for id, end := range map[ports.WindowID]int{1: 0, 2: 400} {
204 if got := rectNow(t, dst, id).X; got <= end-800 || got >= end {
205 t.Fatalf("window %d at %d on the first frame, want between %d and %d", id, got, end-800, end)
206 }
207 }
208 set, _ = r.settleAll(t, wide.Name, other.Name)
209 dst = outScene(t, set, other.Name)
210 if got1, got2 := rectNow(t, dst, 1), rectNow(t, dst, 2); got1.X != 0 || got2.X != 400 {
211 t.Fatalf("settled 1 at %+v and 2 at %+v, want 0 and 400", got1, got2)
212 }
213 r.noFrameScene(t, wide.Name, other.Name)
214}
215
216// The source screen slides to the workspace it falls back on when that one
217// was in the list it showed, else it changes at once.
218func TestMovedWorkspaceSourceSwitchRule(t *testing.T) {
219 r, other := movedRig(t, nil)
220 r.workspaces(t) // window 1 on the first workspace, 2 on the second
221 set := r.key(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift)
222 src := outScene(t, set, wide.Name)
223 // The second workspace was below the moved one: it slides up from there.
224 if got := rectNow(t, src, 2).Y; got != 600 {
225 t.Fatalf("source shows window 2 at y=%d right after the key, want 600 (sliding in)", got)
226 }
227 set, ok := r.clk.flip(t, r.frames, r.scenes, 16*time.Millisecond, wide.Name, other.Name)
228 if !ok {
229 t.Fatal("no frame while the source slides")
230 }
231 if got := rectNow(t, outScene(t, set, wide.Name), 2).Y; got <= 0 || got >= 600 {
232 t.Fatalf("window 2 at y=%d on the first frame, want between 0 and 600", got)
233 }
234 set, _ = r.settleAll(t, wide.Name, other.Name)
235 if got := rectNow(t, outScene(t, set, wide.Name), 2); got.Y != 0 {
236 t.Fatalf("settled window 2 at %+v, want y=0", got)
237 }
238}
239
240// A source whose new workspace was not in the list it showed (the moved one
241// was alone: a fresh empty one replaces it) changes at once: nothing slides
242// in, only the destination's windows move.
243func TestMovedWorkspaceSourceWithoutNeighbourIsInstant(t *testing.T) {
244 r, other := movedRig(t, nil)
245 twoColumns(t, r)
246 set := r.key(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift)
247 if n := len(outScene(t, set, wide.Name).Windows); n != 0 {
248 t.Fatalf("source draws %d windows right after the key, want none", n)
249 }
250 set, ok := r.clk.flip(t, r.frames, r.scenes, 16*time.Millisecond, wide.Name, other.Name)
251 if !ok {
252 t.Fatal("no frame while the windows slide")
253 }
254 if n := len(outScene(t, set, wide.Name).Windows); n != 0 {
255 t.Fatalf("source draws %d windows on the first frame, want none", n)
256 }
257}
258
259// With animations off the moved workspace lands at once on both screens.
260func TestMovedWorkspaceOffIsInstant(t *testing.T) {
261 r, other := movedRig(t, animationsOff)
262 twoColumns(t, r)
263 set := r.key(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift)
264 dst := outScene(t, set, other.Name)
265 if got1, got2 := rectNow(t, dst, 1), rectNow(t, dst, 2); got1.X != 0 || got2.X != 400 {
266 t.Fatalf("1 at %+v and 2 at %+v right after the key, want the settled rects", got1, got2)
267 }
268 r.noFrameScene(t, wide.Name, other.Name)
269}
270
271// A workspace that comes back to the screen it left within the spring of
272// its first move slides its windows only: the screen's running workspace
273// slide (its list still holds the workspace) must not start a vertical one.
274func TestMovedBackWorkspaceHasNoSwitchSlide(t *testing.T) {
275 r, other := movedRig(t, nil)
276 twoColumns(t, r)
277 r.key(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift)
278 // Within the first spring: the source is still sliding.
279 if _, ok := r.clk.flip(t, r.frames, r.scenes, 16*time.Millisecond, wide.Name, other.Name); !ok {
280 t.Fatal("no frame while the windows slide")
281 }
282 set := r.key(t, "Left", ports.ModAlt|ports.ModCtrl|ports.ModShift)
283 dst := outScene(t, set, wide.Name)
284 for id := ports.WindowID(1); id <= 2; id++ {
285 if got := rectNow(t, dst, id); got.Y != 0 {
286 t.Fatalf("window %d at %+v right after moving back, want y=0 (no vertical slide)", id, got)
287 }
288 }
289 set, ok := r.clk.flip(t, r.frames, r.scenes, 16*time.Millisecond, wide.Name, other.Name)
290 if !ok {
291 t.Fatal("no frame while the windows slide back")
292 }
293 dst = outScene(t, set, wide.Name)
294 for id := ports.WindowID(1); id <= 2; id++ {
295 if got := rectNow(t, dst, id); got.Y != 0 {
296 t.Fatalf("window %d at %+v on the first frame, want y=0 (rects slide only)", id, got)
297 }
298 }
299 set, _ = r.settleAll(t, wide.Name, other.Name)
300 dst = outScene(t, set, wide.Name)
301 if got1, got2 := rectNow(t, dst, 1), rectNow(t, dst, 2); got1 != (ports.Rect{W: 400, H: 600}) || got2 != (ports.Rect{X: 400, W: 400, H: 600}) {
302 t.Fatalf("settled 1 at %+v and 2 at %+v", got1, got2)
303 }
304 r.noFrameScene(t, wide.Name, other.Name)
305}
306
307// The start rects are the source's translated by the difference of the
308// outputs' positions on both axes, with the source's size; they settle on
309// the destination's layout.
310func TestMovedWorkspaceStartsFromSourceGeometry(t *testing.T) {
311 big := ports.OutputInfo{Name: "DP-2", Width: 1024, Height: 768, RefreshMilli: 60000}
312 for _, tc := range []struct {
313 name string
314 dst ports.OutputInfo
315 anchor ports.OutputAnchor
316 key string
317 start, final [2]ports.Rect
318 }{
319 {
320 name: "right same size", dst: ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000},
321 anchor: ports.OutputAnchor{Relation: ports.RelationRightOf, To: "DP-1"}, key: "Right",
322 start: [2]ports.Rect{{X: -800, W: 400, H: 600}, {X: -400, W: 400, H: 600}},
323 final: [2]ports.Rect{{W: 400, H: 600}, {X: 400, W: 400, H: 600}},
324 },
325 {
326 name: "below larger", dst: big,
327 anchor: ports.OutputAnchor{Relation: ports.RelationBelow, To: "DP-1"}, key: "Down",
328 start: [2]ports.Rect{{Y: -600, W: 400, H: 600}, {X: 400, Y: -600, W: 400, H: 600}},
329 final: [2]ports.Rect{{W: 512, H: 768}, {X: 512, W: 512, H: 768}},
330 },
331 } {
332 t.Run(tc.name, func(t *testing.T) {
333 r := startSwipeOn(t, func(c *ports.Config) {
334 c.Outputs = []ports.OutputConfig{{Name: "DP-1"}, {Name: "DP-2", Anchor: tc.anchor}}
335 c.Binds["Cmd+Ctrl+Shift+Down"] = string(core.ActionMoveWorkspaceToMonitorDown)
336 }, wide, tc.dst)
337 twoColumns(t, r)
338 dst := outScene(t, r.key(t, tc.key, ports.ModAlt|ports.ModCtrl|ports.ModShift), tc.dst.Name)
339 for i, id := range []ports.WindowID{1, 2} {
340 if got := rectNow(t, dst, id); got != tc.start[i] {
341 t.Fatalf("window %d starts at %+v, want %+v", id, got, tc.start[i])
342 }
343 }
344 set, ok := r.clk.flip(t, r.frames, r.scenes, 16*time.Millisecond, wide.Name, tc.dst.Name)
345 if !ok {
346 t.Fatal("no frame while the windows slide")
347 }
348 if got := rectNow(t, outScene(t, set, tc.dst.Name), 1); got == tc.start[0] || got == tc.final[0] {
349 t.Fatalf("window 1 at %+v on the first frame, want it between start and final", got)
350 }
351 set, _ = r.settleAll(t, wide.Name, tc.dst.Name)
352 dst = outScene(t, set, tc.dst.Name)
353 for i, id := range []ports.WindowID{1, 2} {
354 if got := rectNow(t, dst, id); got != tc.final[i] {
355 t.Fatalf("window %d settles at %+v, want %+v", id, got, tc.final[i])
356 }
357 }
358 })
359 }
360}
361
362// A named workspace pulled to the focused screen from the one showing it
363// moves like a monitor move: its windows slide in from the source.
364func TestPulledNamedWorkspaceSlidesIn(t *testing.T) {
365 r, other := movedRig(t, func(c *ports.Config) {
366 c.Workspaces = []ports.WorkspaceConfig{{Name: "web"}}
367 c.Binds["Alt+w"] = "workspace web"
368 })
369 // web shows on DP-2 with a window, then DP-1 pulls it.
370 r.key(t, "Right", ports.ModAlt|ports.ModCtrl)
371 r.key(t, "w", ports.ModAlt)
372 r.settleAll(t, wide.Name, other.Name)
373 r.mapWindow(t, 1)
374 r.settleAll(t, wide.Name, other.Name)
375 r.key(t, "Left", ports.ModAlt|ports.ModCtrl)
376 set := r.key(t, "w", ports.ModAlt)
377 if got := rectNow(t, outScene(t, set, wide.Name), 1); got.X != 800 {
378 t.Fatalf("window 1 at %+v right after the pull, want x=800 (where DP-2 drew it)", got)
379 }
380 set, ok := r.clk.flip(t, r.frames, r.scenes, 16*time.Millisecond, wide.Name, other.Name)
381 if !ok {
382 t.Fatal("no frame while the window slides")
383 }
384 if got := rectNow(t, outScene(t, set, wide.Name), 1).X; got <= 0 || got >= 800 {
385 t.Fatalf("window 1 at x=%d on the first frame, want between 0 and 800", got)
386 }
387 set, _ = r.settleAll(t, wide.Name, other.Name)
388 if got := rectNow(t, outScene(t, set, wide.Name), 1); got != (ports.Rect{W: 800, H: 600}) {
389 t.Fatalf("settled at %+v", got)
390 }
391}
392
393// Focusing another monitor moves no window: it is not animated.
394func TestFocusMonitorHasNoMotion(t *testing.T) {
395 r, other := movedRig(t, nil)
396 twoColumns(t, r)
397 r.key(t, "Right", ports.ModAlt|ports.ModCtrl)
398 r.noFrameScene(t, wide.Name, other.Name)
399}
400
401// rectNow is the rect of window id in s; it must be in the scene.
402func rectNow(t *testing.T, s ports.Scene, id ports.WindowID) ports.Rect {
403 t.Helper()
404 r, ok := rectOf(s, id)
405 if !ok {
406 t.Fatalf("window %d not shown", id)
407 }
408 return r
409}
410
411// twoColumns maps two windows: 1 on the left half, 2 on the right, focused.
412func twoColumns(t *testing.T, r *swipeRig) ports.Scene {
413 t.Helper()
414 r.mapWindow(t, 1)
415 return r.mapWindow(t, 2)[0]
416}
417
418// Moving a column swaps two windows: the scene right after the key still
419// shows the old rects, a frame an intermediate one, and the end the final.
420func TestMoveColumnRectTransition(t *testing.T) {
421 r := startSwipe(t, nil)
422 twoColumns(t, r)
423 s := r.key(t, "Left", ports.ModAlt|ports.ModShift)[0]
424 if got1, got2 := rectNow(t, s, 1), rectNow(t, s, 2); got1.X != 0 || got2.X != 400 {
425 t.Fatalf("right after the key 1 at %+v and 2 at %+v, want the old 0 and 400", got1, got2)
426 }
427 s = r.frame(t, 16*time.Millisecond)
428 if got := rectNow(t, s, 1).X; got <= 0 || got >= 400 {
429 t.Fatalf("window 1 at %d on the first frame, want between 0 and 400", got)
430 }
431 if got := rectNow(t, s, 2).X; got <= 0 || got >= 400 {
432 t.Fatalf("window 2 at %d on the first frame, want between 0 and 400", got)
433 }
434 s = r.settle(t)
435 if got1, got2 := rectNow(t, s, 1), rectNow(t, s, 2); got1.X != 400 || got2.X != 0 {
436 t.Fatalf("settled 1 at %+v and 2 at %+v, want 400 and 0", got1, got2)
437 }
438}
439
440// A move that also scrolls the view moves a window by the layout delta only:
441// window 1 keeps its place on screen while the view and the others slide.
442func TestMoveColumnThatScrollsKeepsCameraFrame(t *testing.T) {
443 r := startSwipe(t, nil)
444 fourColumns(t, r)
445 r.key(t, "Right", ports.ModAlt|ports.ModShift)
446 r.settle(t) // order 2 1 3 4, view 0
447 s := r.key(t, "Right", ports.ModAlt|ports.ModShift)[0]
448 // Order 2 3 1 4; the view scrolls by 400 to show column 1.
449 for id, want := range map[ports.WindowID]int{2: 0, 1: 400, 3: 800} {
450 if got := rectNow(t, s, id).X; got != want {
451 t.Fatalf("window %d at %d right after the key, want the old %d", id, got, want)
452 }
453 }
454 prev := 800
455 for i := range 40 {
456 s = r.frame(t, 16*time.Millisecond)
457 if i == 39 {
458 t.Fatal("still moving after 40 frames")
459 }
460 if got := rectNow(t, s, 1).X; got < 399 || got > 401 {
461 t.Fatalf("frame %d: window 1 at %d, want it to keep 400 (the scroll is not a layout move)", i, got)
462 }
463 got := rectNow(t, s, 3).X
464 if got > prev {
465 t.Fatalf("frame %d: window 3 at %d after %d", i, got, prev)
466 }
467 if i == 0 && (got <= 0 || got >= 800) {
468 t.Fatalf("window 3 at %d on the first frame, want between 0 and 800", got)
469 }
470 prev = got
471 if got == 0 {
472 break
473 }
474 }
475 for id, want := range map[ports.WindowID]int{2: -400, 3: 0, 1: 400} {
476 if got := rectNow(t, s, id).X; got != want {
477 t.Fatalf("window %d at %d settled, want %d", id, got, want)
478 }
479 }
480}
481
482// A width change animates the frame while the client gets one configure to
483// the final size.
484func TestSetColumnWidthRectTransition(t *testing.T) {
485 r := startSwipe(t, func(c *ports.Config) {
486 c.Layout.MaxColumns = 4
487 c.Binds["Cmd+Ctrl+Right"] = "set-column-width +1"
488 })
489 twoColumns(t, r)
490 r.key(t, "Left", ports.ModAlt) // focus window 1: both columns fit, no scroll
491 for len(r.commands) > 0 {
492 <-r.commands
493 }
494 mods := ports.ModAlt | ports.ModCtrl
495 s := r.key(t, "Right", mods)[0]
496 if got := rectNow(t, s, 1); got.W != 400 {
497 t.Fatalf("window 1 is %d wide right after the key, want the old 400", got.W)
498 }
499 if got := rectNow(t, s, 2); got.X != 400 {
500 t.Fatalf("window 2 at %d right after the key, want the old 400", got.X)
501 }
502 s = r.frame(t, 16*time.Millisecond)
503 if got := rectNow(t, s, 1).W; got <= 400 || got >= 600 {
504 t.Fatalf("window 1 is %d wide on the first frame, want between 400 and 600", got)
505 }
506 if got := rectNow(t, s, 2).X; got <= 400 || got >= 600 {
507 t.Fatalf("window 2 at %d on the first frame, want between 400 and 600", got)
508 }
509 s = r.settle(t)
510 if got := rectNow(t, s, 1).W; got != 600 {
511 t.Fatalf("window 1 is %d wide settled, want 600", got)
512 }
513 if got := rectNow(t, s, 2).X; got != 600 {
514 t.Fatalf("window 2 at %d settled, want 600", got)
515 }
516 var widths []int
517 for len(r.commands) > 0 {
518 if v, ok := (<-r.commands).(ports.ConfigureWindow); ok && v.ID == 1 {
519 widths = append(widths, v.Width)
520 }
521 }
522 if len(widths) != 1 || widths[0] != 600 {
523 t.Fatalf("configures of window 1 carried widths %v, want exactly [600]", widths)
524 }
525}
526
527// Hit-testing follows what is drawn: a point only the shown rect covers
528// reaches that window mid-transition.
529func TestPointerHitsShownRectMidTransition(t *testing.T) {
530 r := startSwipe(t, nil)
531 twoColumns(t, r)
532 // Window 2 (focused) moves to the left half, 1 to the right; drawn, 1
533 // still holds the left half for now.
534 s := r.key(t, "Left", ports.ModAlt|ports.ModShift)[0]
535 if got := rectNow(t, s, 1).X; got != 0 {
536 t.Fatalf("window 1 at %d right after the key, want the old 0", got)
537 }
538 r.input <- ports.PointerMotion{X: 100, Y: 300}
539 r.input <- ports.PointerButton{Button: 0x110, Pressed: true}
540 s = scene(t, r.scenes)
541 for _, w := range s.Windows {
542 if w.Focused != (w.ID == 1) {
543 t.Fatalf("window %d focused %t after a click on the shown window 1", w.ID, w.Focused)
544 }
545 }
546 r.input <- ports.PointerButton{Button: 0x110}
547}
548
549// toggle-fullscreen grows the window to the output over the frames.
550func TestFullscreenRectTransition(t *testing.T) {
551 r := startSwipe(t, nil)
552 twoColumns(t, r)
553 s := r.key(t, "f", ports.ModAlt|ports.ModShift)[0]
554 if got := rectNow(t, s, 2); got.W != 400 {
555 t.Fatalf("window 2 is %d wide right after the key, want the old 400", got.W)
556 }
557 got := rectNow(t, r.frame(t, 16*time.Millisecond), 2)
558 if got.W <= 400 || got.W >= 800 {
559 t.Fatalf("window 2 is %d wide on the first frame, want between 400 and 800", got.W)
560 }
561 if got := rectNow(t, r.settle(t), 2); got != (ports.Rect{W: 800, H: 600}) {
562 t.Fatalf("window 2 at %+v settled, want the output", got)
563 }
564}
565
566// With animations off a layout change lands at once.
567func TestAnimationsOffRectsLandAtOnce(t *testing.T) {
568 r := startSwipe(t, animationsOff)
569 twoColumns(t, r)
570 s := r.key(t, "Left", ports.ModAlt|ports.ModShift)[0]
571 if got1, got2 := rectNow(t, s, 1), rectNow(t, s, 2); got1.X != 400 || got2.X != 0 {
572 t.Fatalf("1 at %+v and 2 at %+v right after the key, want the final 400 and 0", got1, got2)
573 }
574 r.noFrameScene(t)
575}
576
577// A second action during a rect transition starts from what is drawn: the
578// scene right after it shows the rects of the last frame.
579func TestChainedMovesStartFromShownRects(t *testing.T) {
580 r := startSwipe(t, nil)
581 twoColumns(t, r)
582 r.key(t, "Left", ports.ModAlt|ports.ModShift)
583 mid := r.frame(t, 16*time.Millisecond)
584 s := r.key(t, "Right", ports.ModAlt|ports.ModShift)[0]
585 for _, id := range []ports.WindowID{1, 2} {
586 if got, want := rectNow(t, s, id), rectNow(t, mid, id); got != want {
587 t.Fatalf("window %d at %+v right after the second key, want the shown %+v", id, got, want)
588 }
589 }
590 end := r.settle(t)
591 if got1, got2 := rectNow(t, end, 1), rectNow(t, end, 2); got1.X != 0 || got2.X != 400 {
592 t.Fatalf("settled 1 at %+v and 2 at %+v, want 0 and 400", got1, got2)
593 }
594}
595
596// A click on a column that is partly off screen focuses it: the scene right
597// after the click still shows the old view, a frame an intermediate one and
598// the settled scene the new one.
599func TestClickFocusTransition(t *testing.T) {
600 r := startSwipe(t, func(c *ports.Config) {
601 c.Layout.MaxColumns = 4
602 c.Binds["Cmd+Ctrl+Right"] = "set-column-width +2"
603 })
604 threeColumns(t, r)
605 // Column 3 grows from 1 to 3 cells of 200 px: column 2 sticks out 66 px
606 // on the left.
607 r.key(t, "Right", ports.ModAlt|ports.ModCtrl)
608 if got := xOf(t, r.settle(t), 2); got != -66 {
609 t.Fatalf("column 2 at %d, want -66", got)
610 }
611 // The release of the modifiers: a plain click, not a window drag.
612 r.input <- ports.KeyEvent{Keysym: "Alt_L"}
613 r.input <- ports.PointerMotion{X: 100, Y: 300}
614 r.drain()
615 r.input <- ports.PointerButton{Button: 0x110, Pressed: true}
616 s := scene(t, r.scenes)
617 if got := xOf(t, s, 2); got != -66 {
618 t.Fatalf("column 2 at %d right after the click, want the old -66", got)
619 }
620 got := xOf(t, r.frame(t, 16*time.Millisecond), 2)
621 if got <= -66 || got >= 0 {
622 t.Fatalf("column 2 at %d on the first frame, want between -66 and 0", got)
623 }
624 if got := xOf(t, r.settle(t), 2); got != 0 {
625 t.Fatalf("column 2 at %d settled, want 0", got)
626 }
627}
628
629// A discrete swipe step (fixed overflow: nothing follows the fingers) moves
630// through a rect motion: the scene right after the lift shows the old rects.
631func TestDiscreteSwipeTransition(t *testing.T) {
632 r := startSwipe(t, func(c *ports.Config) { c.Layout.Overflow = "fixed" })
633 r.mapWindow(t, 1)
634 r.mapWindow(t, 2)
635 r.key(t, "f", ports.ModAlt) // maximize window 2
636 if got := rectNow(t, r.settle(t), 2); got.W != 800 {
637 t.Fatalf("window 2 is %d wide, want maximized 800", got.W)
638 }
639 r.begin()
640 for range 10 {
641 r.at += 8 * time.Millisecond
642 r.input <- ports.SwipeUpdate{DX: -40, Time: r.at}
643 }
644 s := r.end(t, false)
645 if got := rectNow(t, s, 2); got.W != 800 {
646 t.Fatalf("window 2 is %d wide right after the lift, want the old 800", got.W)
647 }
648 got := rectNow(t, r.frame(t, 16*time.Millisecond), 2)
649 if got.W <= 400 || got.W >= 800 {
650 t.Fatalf("window 2 is %d wide on the first frame, want between 400 and 800", got.W)
651 }
652 s = r.settle(t)
653 if got1, got2 := rectNow(t, s, 1), rectNow(t, s, 2); got1.W != 400 || got2.X != 400 || got2.W != 400 {
654 t.Fatalf("settled 1 at %+v and 2 at %+v, want 1 beside 2 on halves", got1, got2)
655 }
656}
657
658// A window unmapped while its rect motion runs stops moving: with
659// animations on it stays in the scene only as a leaving entry (drawn where
660// it was, fading out and shrinking, hidden to input), the others keep their
661// motions, and once everything settles it is gone and no frame is wanted:
662// a motion kept for a gone window would ask for frames with nothing to
663// move. With animations off it leaves the scene at once.
664func TestUnmapMidRectMotion(t *testing.T) {
665 for _, tt := range []struct {
666 name string
667 edit func(*ports.Config)
668 }{{"on", nil}, {"off", animationsOff}} {
669 t.Run(tt.name, func(t *testing.T) {
670 r := startSwipe(t, tt.edit)
671 threeColumns(t, r)
672 // 3, focused, swaps with 2: both move.
673 mid := r.key(t, "Left", ports.ModAlt|ports.ModShift)[0]
674 if tt.edit == nil {
675 // Off: the swap is instant, a flip draws no new scene.
676 mid = r.frame(t, 16*time.Millisecond)
677 if x := rectNow(t, mid, 3).X; x <= 0 || x >= 400 {
678 t.Fatalf("window 3 at %d on the first frame, want it moving", x)
679 }
680 }
681 moving := rectNow(t, mid, 2).X
682 drawn3 := rectNow(t, mid, 3)
683 r.client <- ports.WindowUnmapped{ID: 3}
684 s := scene(t, r.scenes)
685 w3, shown3 := listedIn(s, 3)
686 switch {
687 case tt.edit != nil && shown3:
688 t.Fatalf("the unmapped window is still shown with animations off: %+v", w3)
689 case tt.edit == nil && (!shown3 || w3.Fade != 0 || w3.Rect != drawn3):
690 // It leaves from where it was drawn, not from the settled
691 // slot it was moving to.
692 t.Fatalf("the unmapped window right after the unmap: %+v (shown %t), want it leaving from %+v", w3, shown3, drawn3)
693 }
694 // The neighbour's own motion carries on from where it was drawn.
695 if got := rectNow(t, s, 2).X; got != moving {
696 t.Fatalf("window 2 jumped from %d to %d on the unmap", moving, got)
697 }
698 if tt.edit == nil {
699 next := r.frame(t, 16*time.Millisecond)
700 if got := rectNow(t, next, 2).X; got == moving {
701 t.Fatalf("window 2 stopped at %d: its motion died with the unmapped window", got)
702 }
703 if w, ok := listedIn(next, 3); !ok || !(w.Fade > 0 && w.Fade < 1) || w.Rect.W >= 400 {
704 t.Fatalf("the leaving window on the next frame: %+v (shown %t), want it fading and shrinking", w, ok)
705 }
706 } else if moving != 400 {
707 t.Fatalf("window 2 at %d with animations off, want 400", moving)
708 }
709 if tt.edit == nil {
710 s = r.settle(t)
711 }
712 if _, ok := listedIn(s, 3); ok {
713 t.Fatal("the unmapped window came back")
714 }
715 if got := rectNow(t, s, 2).X; got != 400 {
716 t.Fatalf("window 2 settled at %d, want 400", got)
717 }
718 r.noFrameScene(t)
719 })
720 }
721}
722
723// listedIn is the scene window id, shown or not.
724func listedIn(s ports.Scene, id ports.WindowID) (ports.SceneWindow, bool) {
725 for _, w := range s.Windows {
726 if w.ID == id {
727 return w, true
728 }
729 }
730 return ports.SceneWindow{}, false
731}