1package core_test
2
3import (
4 "context"
5 "slices"
6 "testing"
7 "time"
8
9 "github.com/bnema/neferwl/internal/core"
10 "github.com/bnema/neferwl/internal/ports"
11)
12
13// swipeRig runs core on one 800x600 output with no gaps or border, a
14// clock the test moves, and page flips the test sends.
15type swipeRig struct {
16 *multiRig
17 frames chan ports.OutputFrame
18 clk *stepClock
19 // at is the device time of the next swipe event.
20 at time.Duration
21 // outs are the plugged outputs, for landed.
22 outs []string
23}
24
25var wide = ports.OutputInfo{Name: "DP-1", Width: 800, Height: 600, RefreshMilli: 60000}
26
27func startSwipe(t *testing.T, edit func(*ports.Config)) *swipeRig {
28 t.Helper()
29 return startSwipeOn(t, edit, wide)
30}
31
32// startSwipeOn is startSwipe with the given outputs plugged in order.
33func startSwipeOn(t *testing.T, edit func(*ports.Config), outs ...ports.OutputInfo) *swipeRig {
34 t.Helper()
35 cfg := altCmdDefaults()
36 cfg.Border.Width = 0
37 cfg.Layout.Gaps = 0
38 cfg.Terminal.AutoOpen = "never"
39 // The rig steps animations frame by frame: they are on unless edit
40 // turns them off.
41 cfg.Animations.On = true
42 if edit != nil {
43 edit(&cfg)
44 }
45 r := &swipeRig{frames: make(chan ports.OutputFrame), clk: newStepClock(t)}
46 r.multiRig = &multiRig{
47 client: make(chan ports.ClientEvent, 16), input: make(chan ports.InputEvent, 64),
48 output: make(chan ports.OutputEvent, 4), reload: make(chan ports.ConfigChanged, 4),
49 commands: make(chan ports.ClientCommand, 1024), scenes: make(chan []ports.Scene, 1), spawn: make(chan ports.SpawnRequest, 16), state: make(chan ports.State, 1), cfg: cfg,
50 }
51 c, err := core.New(cfg, core.Channels{Client: r.client, Input: r.input, Output: r.output, Config: r.reload, Commands: r.commands, Scenes: r.scenes, Spawn: r.spawn, State: r.state, Frames: r.frames}, core.Options{Clock: r.clk.clock})
52 if err != nil {
53 t.Fatal(err)
54 }
55 ctx, cancel := context.WithCancel(context.Background())
56 done := make(chan error, 1)
57 go func() { done <- c.Run(ctx) }()
58 t.Cleanup(func() { cancel(); <-done })
59 for _, o := range outs {
60 r.plug(t, o)
61 r.outs = append(r.outs, o.Name)
62 }
63 return r
64}
65
66func (r *swipeRig) advance(d time.Duration) {
67 r.clk.advance(d)
68}
69
70// mapWindow maps a window and, with animations on, lands its entrance (and
71// the neighbours' re-flow) so the scene is the settled one: the tests of a
72// swipe rig look at what the map leaves. The entrance itself is checked on
73// the raw multiRig.mapWindow (TestMap*).
74func (r *swipeRig) mapWindow(t *testing.T, id ports.WindowID) []ports.Scene {
75 t.Helper()
76 set := r.multiRig.mapWindow(t, id)
77 if landed, ok := r.settleAll(t, r.outs...); ok {
78 return landed
79 }
80 return set
81}
82
83// begin starts a swipe; nothing moves until it picks an axis.
84func (r *swipeRig) begin() {
85 r.at += time.Second
86 r.input <- ports.SwipeBegin{Fingers: 3, Time: r.at}
87}
88
89// move sends one swipe update and returns the scene it produced.
90func (r *swipeRig) move(t *testing.T, dx, dy float64) ports.Scene {
91 t.Helper()
92 r.at += 8 * time.Millisecond
93 r.input <- ports.SwipeUpdate{DX: dx, DY: dy, Time: r.at}
94 return scene(t, r.scenes)
95}
96
97func (r *swipeRig) end(t *testing.T, cancelled bool) ports.Scene {
98 t.Helper()
99 r.input <- ports.SwipeEnd{Cancelled: cancelled, Time: r.at}
100 return scene(t, r.scenes)
101}
102
103// frame moves the clock by d, sends a flip and returns the next scene.
104func (r *swipeRig) frame(t *testing.T, d time.Duration) ports.Scene {
105 t.Helper()
106 r.advance(d)
107 r.frames <- ports.OutputFrame{Output: wide.Name}
108 return scene(t, r.scenes)
109}
110
111// settle moves the clock past any spring and returns the settled scene.
112func (r *swipeRig) settle(t *testing.T) ports.Scene {
113 t.Helper()
114 return r.frame(t, 5*time.Second)
115}
116
117// settleAll moves the clock past every spring, flips the outputs and
118// returns the scene set that results (ok false: nothing was animating).
119func (r *swipeRig) settleAll(t *testing.T, outputs ...string) ([]ports.Scene, bool) {
120 t.Helper()
121 return r.clk.settle(t, r.frames, r.scenes, outputs...)
122}
123
124// flick sends a quick swipe of n updates of (dx, dy) and lifts the
125// fingers; it returns the scene right after the lift.
126func (r *swipeRig) flick(t *testing.T, n int, dx, dy float64) ports.Scene {
127 t.Helper()
128 r.begin()
129 for range n {
130 r.move(t, dx, dy)
131 }
132 return r.end(t, false)
133}
134
135// workspaces puts window 1 on the first workspace and 2 on the second,
136// showing the first.
137func (r *swipeRig) workspaces(t *testing.T) {
138 t.Helper()
139 r.mapWindow(t, 1)
140 r.keySettled(t, "Next")
141 r.mapWindow(t, 2)
142 r.keySettled(t, "Prior")
143}
144
145// keySettled presses a workspace key and waits for the slide it starts, so
146// the next step begins from the settled view.
147func (r *swipeRig) keySettled(t *testing.T, sym string) {
148 t.Helper()
149 r.key(t, sym, ports.ModAlt)
150 if r.cfg.Animations.On {
151 r.settle(t)
152 }
153}
154
155func rectOf(s ports.Scene, id ports.WindowID) (ports.Rect, bool) {
156 for _, w := range s.Windows {
157 if w.ID == id && !w.Hidden {
158 return w.Rect, true
159 }
160 }
161 return ports.Rect{}, false
162}
163
164func (r *swipeRig) focused(t *testing.T) ports.WindowID {
165 t.Helper()
166 var id ports.WindowID
167 for len(r.commands) > 0 {
168 if v, ok := (<-r.commands).(ports.FocusWindow); ok {
169 id = v.ID
170 }
171 }
172 return id
173}
174
175// threeColumns maps three windows of half the width; the view shows 2
176// and 3, with 3 focused. It returns the last scene.
177func threeColumns(t *testing.T, r *swipeRig) ports.Scene {
178 t.Helper()
179 var s []ports.Scene
180 for id := ports.WindowID(1); id <= 3; id++ {
181 s = r.mapWindow(t, id)
182 }
183 return s[0]
184}
185
186func TestSwipeFollowsFingers(t *testing.T) {
187 r := startSwipe(t, nil)
188 before, _ := rectOf(threeColumns(t, r), 2)
189 r.begin()
190 // Under the 16 unit threshold the swipe has not picked its axis:
191 // that movement is not applied (as in GNOME Shell and niri).
192 r.input <- ports.SwipeUpdate{DX: -10, Time: r.at}
193 s := r.move(t, -10, 0)
194 after, _ := rectOf(s, 2)
195 // 10 units of 1200 per 800 px view are 7 px; near the resting view
196 // the detent holds the view back a little.
197 if d := after.X - before.X; d <= 0 || d >= 7 {
198 t.Fatalf("column moved %d px with the fingers", d)
199 }
200 // Halfway to the next column edge (400 px: 600 units) the view has
201 // caught up with the fingers; a full step shows whole.
202 s = r.move(t, -290, 0)
203 if got, _ := rectOf(s, 2); got.X-before.X != 200 {
204 t.Fatalf("column moved %d px halfway", got.X-before.X)
205 }
206 s = r.move(t, -300, 0)
207 if got, _ := rectOf(s, 2); got.X-before.X != 400 {
208 t.Fatalf("column moved %d px after a full step", got.X-before.X)
209 }
210}
211
212func TestSwipeFlickLandsOnColumnWithSpring(t *testing.T) {
213 r := startSwipe(t, nil)
214 threeColumns(t, r)
215 // Focus is on 3 (right); the view shows 2 and 3. Flick right to left
216 // fingers (content moves left in classic scroll): view goes left.
217 r.focused(t)
218 s := r.flick(t, 10, -40, 0)
219 mid, ok := rectOf(s, 1)
220 if !ok {
221 t.Fatal("column 1 not shown while the view slides")
222 }
223 if mid.X == 0 {
224 t.Fatal("view jumped: no slide")
225 }
226 s = r.frame(t, 16*time.Millisecond)
227 next, _ := rectOf(s, 1)
228 if next.X <= mid.X || next.X > 0 {
229 t.Fatalf("spring does not approach the target: %d then %d", mid.X, next.X)
230 }
231 s = r.settle(t)
232 if got, _ := rectOf(s, 1); got.X != 0 {
233 t.Fatalf("landed at %d, want column 1 at the left edge", got.X)
234 }
235 if id := r.focused(t); id != 1 && id != 2 {
236 t.Fatalf("focus %d after swiping left", id)
237 }
238}
239
240func TestSwipeNaturalScrollInverts(t *testing.T) {
241 for _, natural := range []bool{false, true} {
242 r := startSwipe(t, func(c *ports.Config) { c.Touchpad.NaturalScroll = natural })
243 threeColumns(t, r)
244 dx := -40.0
245 if natural {
246 dx = 40
247 }
248 r.flick(t, 10, dx, 0)
249 s := r.settle(t)
250 if got, _ := rectOf(s, 1); got.X != 0 {
251 t.Fatalf("natural=%t: column 1 at %d", natural, got.X)
252 }
253 }
254}
255
256func TestSwipeWorkspaceSlides(t *testing.T) {
257 r := startSwipe(t, nil)
258 r.workspaces(t)
259 r.begin()
260 s := r.move(t, 0, 30)
261 one, ok1 := rectOf(s, 1)
262 two, ok2 := rectOf(s, 2)
263 if !ok1 || !ok2 {
264 t.Fatalf("both workspaces show while sliding: %v %v", ok1, ok2)
265 }
266 if one.Y >= 0 || two.Y <= 0 || two.Y-one.Y != 600 {
267 t.Fatalf("slide positions %d %d", one.Y, two.Y)
268 }
269 for range 8 {
270 r.move(t, 0, 30)
271 }
272 s = r.end(t, false)
273 if _, ok := rectOf(s, 2); !ok {
274 t.Fatal("target workspace hidden while landing")
275 }
276 s = r.settle(t)
277 if _, ok := rectOf(s, 1); ok {
278 t.Fatal("old workspace still shown")
279 }
280 if got, _ := rectOf(s, 2); got.Y != 0 {
281 t.Fatalf("workspace 2 at %d", got.Y)
282 }
283}
284
285func TestSwipeWorkspaceCancelReturns(t *testing.T) {
286 r := startSwipe(t, nil)
287 r.workspaces(t)
288 r.begin()
289 for range 10 {
290 r.move(t, 0, 40)
291 }
292 r.end(t, true)
293 s := r.settle(t)
294 if got, ok := rectOf(s, 1); !ok || got.Y != 0 {
295 t.Fatalf("cancelled swipe left workspace 1 at %v %t", got, ok)
296 }
297}
298
299func TestSwipeFixedOverflowRunsFocusAction(t *testing.T) {
300 r := startSwipe(t, func(c *ports.Config) { c.Layout.Overflow = "fixed" })
301 r.mapWindow(t, 1)
302 r.mapWindow(t, 2)
303 r.focused(t)
304 // The view cannot slide: nothing moves until the fingers lift.
305 r.begin()
306 for range 10 {
307 r.at += 8 * time.Millisecond
308 r.input <- ports.SwipeUpdate{DX: -40, Time: r.at}
309 }
310 r.end(t, false)
311 if id := r.focused(t); id != 1 {
312 t.Fatalf("fixed overflow swipe left focuses %d", id)
313 }
314}
315
316func TestSwipeShortSwipeDoesNothingInFixedOverflow(t *testing.T) {
317 r := startSwipe(t, func(c *ports.Config) { c.Layout.Overflow = "fixed" })
318 r.mapWindow(t, 1)
319 r.mapWindow(t, 2)
320 r.focused(t)
321 r.begin()
322 r.input <- ports.SwipeUpdate{DX: -20, Time: r.at + time.Second}
323 r.input <- ports.SwipeEnd{Time: r.at + 2*time.Second}
324 r.key(t, "Return", 0)
325 if id := r.focused(t); id != 0 {
326 t.Fatalf("slow short swipe moved focus to %d", id)
327 }
328}
329
330func TestKeyDuringSlideRetargets(t *testing.T) {
331 r := startSwipe(t, nil)
332 threeColumns(t, r)
333 s := r.flick(t, 10, -40, 0)
334 landing, _ := rectOf(s, 2)
335 // Back to column 3: the view heads there from where it is, no jump.
336 r.key(t, "Right", ports.ModAlt)
337 s = r.key(t, "Right", ports.ModAlt)[0]
338 if got, _ := rectOf(s, 2); got.X != landing.X {
339 t.Fatalf("view jumped from %d to %d", landing.X, got.X)
340 }
341 // The retargeted spring starts on the next frame.
342 r.frame(t, 16*time.Millisecond)
343 s = r.settle(t)
344 if got, _ := rectOf(s, 3); got.X != 400 {
345 t.Fatalf("column 3 landed at %d", got.X)
346 }
347}
348
349func TestSwipeFromEmptyWorkspaceLandsWithoutJump(t *testing.T) {
350 r := startSwipe(t, nil)
351 r.workspaces(t)
352 // Workspace 3 is the trailing empty one; start there and swipe up to 2.
353 r.keySettled(t, "Next")
354 r.keySettled(t, "Next")
355 r.begin()
356 var s ports.Scene
357 for range 6 {
358 s = r.move(t, 0, -40)
359 }
360 before, ok := rectOf(s, 2)
361 if !ok {
362 t.Fatal("workspace 2 not sliding in")
363 }
364 // Landing drops no numbered workspace here (the trailing one stays),
365 // but the view must not jump either way.
366 s = r.end(t, false)
367 after, _ := rectOf(s, 2)
368 if after.Y != before.Y {
369 t.Fatalf("landing jumped from %d to %d", before.Y, after.Y)
370 }
371 if got, _ := rectOf(r.settle(t), 2); got.Y != 0 {
372 t.Fatalf("workspace 2 landed at %d", got.Y)
373 }
374}
375
376func TestSwipeLandingKeepsViewWhenEmptyWorkspaceDrops(t *testing.T) {
377 r := startSwipe(t, nil)
378 // 1 on the first workspace, 2 on the third; the second is empty and
379 // active, so leaving it drops it and renumbers the third.
380 r.mapWindow(t, 1)
381 r.key(t, "Next", ports.ModAlt)
382 r.key(t, "Next", ports.ModAlt)
383 r.mapWindow(t, 2)
384 r.key(t, "Prior", ports.ModAlt)
385 r.begin()
386 var s ports.Scene
387 for range 4 {
388 s = r.move(t, 0, 20)
389 }
390 before, ok := rectOf(s, 2)
391 if !ok {
392 t.Fatal("workspace 3 not sliding in")
393 }
394 s = r.end(t, false)
395 after, ok := rectOf(s, 2)
396 if !ok || after.Y != before.Y {
397 t.Fatalf("landing moved workspace 3 from %d to %d (shown %t)", before.Y, after.Y, ok)
398 }
399 if got, _ := rectOf(r.settle(t), 2); got.Y != 0 {
400 t.Fatalf("landed at %d", got.Y)
401 }
402}
403
404func TestSwipeDropsWhenWorkspacesChange(t *testing.T) {
405 r := startSwipe(t, nil)
406 r.workspaces(t)
407 r.begin()
408 r.move(t, 0, 30)
409 // A key switches workspace mid-swipe: the swipe lets go at once.
410 s := r.key(t, "Next", ports.ModAlt)[0]
411 if got, ok := rectOf(s, 2); !ok || got.Y == 0 {
412 t.Fatalf("workspace 2 at %v %t after the key, want the old position", got, ok)
413 }
414 s = r.settle(t)
415 if got, ok := rectOf(s, 2); !ok || got.Y != 0 {
416 t.Fatalf("workspace 2 at %v %t after the key", got, ok)
417 }
418 if _, ok := rectOf(s, 1); ok {
419 t.Fatal("workspace 1 still sliding")
420 }
421 // The rest of the swipe moves nothing: its end leaves the view as is.
422 r.at += 8 * time.Millisecond
423 r.input <- ports.SwipeUpdate{DY: 30, Time: r.at}
424 if got, ok := rectOf(r.end(t, false), 2); !ok || got.Y != 0 {
425 t.Fatalf("workspace 2 at %v %t after the swipe", got, ok)
426 }
427}
428
429func TestSlideMovesPointerFocus(t *testing.T) {
430 r := startSwipe(t, nil)
431 threeColumns(t, r)
432 // The pointer rests over column 2 (x 100: the view shows 2 and 3).
433 r.input <- ports.PointerMotion{X: 100, Y: 300}
434 r.drain()
435 r.flick(t, 10, -40, 0)
436 r.settle(t)
437 var focus ports.PointerFocus
438 var motion ports.PointerMotionTo
439 // Core sends the pointer update right after the scene.
440 for _, v := range r.sent() {
441 switch v := v.(type) {
442 case ports.PointerFocus:
443 focus = v
444 case ports.PointerMotionTo:
445 motion = v
446 }
447 }
448 // Column 1 slid under the still pointer; it follows the window.
449 if focus.ID != 1 {
450 t.Fatalf("pointer focus %+v", focus)
451 }
452 if motion.ID != 1 || motion.X != 100 || motion.DX != 0 {
453 t.Fatalf("last motion %+v", motion)
454 }
455}
456
457func TestSwipeBeginWithoutEndSettlesTheFirst(t *testing.T) {
458 r := startSwipe(t, nil)
459 threeColumns(t, r)
460 r.begin()
461 s := r.move(t, -40, 0)
462 moved, _ := rectOf(s, 2)
463 // A begin without the previous end: the first swipe springs back.
464 r.begin()
465 scene(t, r.scenes)
466 s = r.settle(t)
467 if got, _ := rectOf(s, 2); got.X == moved.X || got.X != 0 {
468 t.Fatalf("first swipe left column 2 at %d", got.X)
469 }
470}
471
472func TestSwipeIgnoredAfterSwitchingAwayAndBack(t *testing.T) {
473 r := startSwipe(t, nil)
474 before, _ := rectOf(threeColumns(t, r), 2)
475 r.begin()
476 r.move(t, -30, 0)
477 r.key(t, "Next", ports.ModAlt)
478 r.key(t, "Prior", ports.ModAlt)
479 // The same workspace is back, but the swipe was let go.
480 r.at += 8 * time.Millisecond
481 r.input <- ports.SwipeUpdate{DX: -30, Time: r.at}
482 if got, _ := rectOf(r.end(t, false), 2); got != before {
483 t.Fatalf("column 2 at %v, want %v", got, before)
484 }
485}
486
487func TestSwipeKeptWhenAnotherOutputSwitchesWorkspace(t *testing.T) {
488 r := startSwipe(t, nil)
489 before, _ := rectOf(threeColumns(t, r), 2)
490 r.plug(t, ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000})
491 // Window 4 on DP-2, so it has a workspace below to switch to.
492 r.input <- ports.PointerMotion{X: 1000, Y: 300}
493 scene(t, r.scenes)
494 r.mapWindow(t, 4)
495 r.input <- ports.PointerMotion{X: 100, Y: 300}
496 scene(t, r.scenes)
497 r.drain()
498 r.begin()
499 r.move(t, -30, 0)
500 // The pointer goes to DP-2 and a bind switches its workspace.
501 r.input <- ports.PointerMotion{X: 1000, Y: 300}
502 scene(t, r.scenes)
503 r.key(t, "Next", ports.ModAlt)
504 // DP-2 slides to its other workspace on its own flips.
505 r.advance(5 * time.Second)
506 r.frames <- ports.OutputFrame{Output: "DP-2"}
507 for _, sc := range receive(t, r.scenes) {
508 if _, ok := rectOf(sc, 4); ok {
509 t.Fatal("DP-2 did not switch workspace")
510 }
511 }
512 // The swipe on DP-1 still follows the fingers.
513 var got ports.Rect
514 for _, sc := range receiveMove(t, r, -570) {
515 if sc.Output == wide.Name {
516 got, _ = rectOf(sc, 2)
517 }
518 }
519 // 600 units at 2/3 px each: a full step of 400 px.
520 if got.X-before.X != 400 {
521 t.Fatalf("column 2 moved %d px", got.X-before.X)
522 }
523}
524
525// receiveMove sends one swipe update and returns every output's scene.
526func receiveMove(t *testing.T, r *swipeRig, dx float64) []ports.Scene {
527 t.Helper()
528 r.at += 8 * time.Millisecond
529 r.input <- ports.SwipeUpdate{DX: dx, Time: r.at}
530 return receive(t, r.scenes)
531}
532
533func TestDiscreteSwipeSkippedWhenPointerChangesOutput(t *testing.T) {
534 r := startSwipe(t, func(c *ports.Config) { c.Layout.Overflow = "fixed" })
535 r.mapWindow(t, 1)
536 r.mapWindow(t, 2)
537 r.plug(t, ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000})
538 // Windows 3 and 4 on DP-2, 4 focused there.
539 r.input <- ports.PointerMotion{X: 1000, Y: 300}
540 scene(t, r.scenes)
541 r.mapWindow(t, 3)
542 r.mapWindow(t, 4)
543 // The swipe starts on DP-1 ...
544 r.input <- ports.PointerMotion{X: 100, Y: 300}
545 scene(t, r.scenes)
546 r.begin()
547 for range 10 {
548 r.at += 8 * time.Millisecond
549 r.input <- ports.SwipeUpdate{DX: -40, Time: r.at}
550 }
551 // ... and the pointer takes the focus to DP-2 before the fingers lift.
552 r.input <- ports.PointerMotion{X: 1000, Y: 300}
553 scene(t, r.scenes)
554 r.drain()
555 r.input <- ports.SwipeEnd{Time: r.at}
556 for _, v := range r.sent() {
557 if f, ok := v.(ports.FocusWindow); ok && f.ID == 3 {
558 t.Fatal("swipe from DP-1 moved the focus on DP-2")
559 }
560 }
561}
562
563// drain waits for core to handle what was sent, then empties commands.
564func (r *swipeRig) drain() {
565 for range r.sent() {
566 }
567}
568
569// sent waits for core to handle what was sent and returns the commands it
570// sent meanwhile (the sync flip is a barrier, see published).
571func (r *swipeRig) sent() []ports.ClientCommand {
572 send(r.frames, ports.OutputFrame{Output: "sync"})
573 var out []ports.ClientCommand
574 for {
575 select {
576 case v := <-r.commands:
577 out = append(out, v)
578 default:
579 return out
580 }
581 }
582}
583
584// In the overview a three-finger swipe does not slide the view: nothing
585// is drawn while the fingers move, and when they lift the selection moves
586// one column, the previews where they were.
587func TestSwipeInOverview(t *testing.T) {
588 r := startSwipe(t, nil)
589 threeColumns(t, r)
590 opened := r.key(t, "o", ports.ModAlt)[0]
591 selected := func(s ports.Scene) ports.WindowID {
592 for _, w := range s.Windows {
593 if w.Focused && w.Preview > 0 {
594 return w.ID
595 }
596 }
597 return 0
598 }
599 r.begin()
600 for range 3 {
601 r.at += 8 * time.Millisecond
602 r.input <- ports.SwipeUpdate{DX: -40, Time: r.at}
603 }
604 sceneMatch(t, r.scenes, func(s ports.Scene) bool { return selected(s) == 2 })
605 s := r.end(t, false)
606 if id := selected(s); id != 2 {
607 t.Fatalf("selected %d after a left swipe, want 2", id)
608 }
609 before, _ := rectOf(opened, 2)
610 if after, _ := rectOf(s, 2); before != after {
611 t.Fatalf("preview moved: %+v then %+v", before, after)
612 }
613 // A small swipe does nothing: the l after it moves from 2 to 3.
614 r.begin()
615 r.at += 8 * time.Millisecond
616 r.input <- ports.SwipeUpdate{DX: -20, Time: r.at}
617 if id := selected(r.end(t, false)); id != 2 {
618 t.Fatalf("selected %d after a small swipe", id)
619 }
620 if id := selected(r.key(t, "l", 0)[0]); id != 3 {
621 t.Fatalf("selected %d after a small swipe and l, want 3", id)
622 }
623}
624
625// A four-finger swipe up opens the overview and down closes it on the
626// selection, whatever natural-scroll says (as niri). A short one or one
627// sideways does nothing.
628func TestFourFingerSwipeOverview(t *testing.T) {
629 for _, natural := range []bool{false, true} {
630 r := startSwipe(t, func(c *ports.Config) { c.Touchpad.NaturalScroll = natural })
631 threeColumns(t, r)
632 four := func(dx, dy float64, n int) ports.Scene {
633 r.at += time.Second
634 r.input <- ports.SwipeBegin{Fingers: 4, Time: r.at}
635 for range n {
636 r.at += 8 * time.Millisecond
637 r.input <- ports.SwipeUpdate{DX: dx, DY: dy, Time: r.at}
638 }
639 return r.end(t, false)
640 }
641 inOverview := func(s ports.Scene) bool {
642 for _, w := range s.Windows {
643 if w.Preview > 0 {
644 return true
645 }
646 }
647 return false
648 }
649 if inOverview(four(0, -5, 4)) {
650 t.Fatalf("natural=%v: short swipe opened the overview", natural)
651 }
652 if inOverview(four(-40, 0, 10)) {
653 t.Fatalf("natural=%v: sideways swipe opened the overview", natural)
654 }
655 if !inOverview(four(0, -40, 10)) {
656 t.Fatalf("natural=%v: swipe up did not open the overview", natural)
657 }
658 if !inOverview(four(0, -40, 10)) {
659 t.Fatalf("natural=%v: a second swipe up closed it", natural)
660 }
661 r.key(t, "h", 0)
662 s := four(0, 40, 10)
663 if inOverview(s) {
664 t.Fatalf("natural=%v: swipe down did not close the overview", natural)
665 }
666 if id := r.focused(t); id != 2 {
667 t.Fatalf("natural=%v: closed on %d, want the selection 2", natural, id)
668 }
669 }
670}
671
672// four lifts a four-finger swipe of n updates of (dx, dy) after the
673// fingers went down, and returns the scene right after the lift.
674func (r *swipeRig) four(t *testing.T, dx, dy float64, n int) ports.Scene {
675 t.Helper()
676 r.at += time.Second
677 r.input <- ports.SwipeBegin{Fingers: 4, Time: r.at}
678 for range n {
679 r.at += 8 * time.Millisecond
680 r.input <- ports.SwipeUpdate{DX: dx, DY: dy, Time: r.at}
681 }
682 return r.end(t, false)
683}
684
685// stashedThird maps three windows and stashes the third (shown, focused),
686// settled.
687func stashedThird(t *testing.T, r *swipeRig) ports.Scene {
688 t.Helper()
689 threeColumns(t, r)
690 s := r.key(t, "s", ports.ModAlt|ports.ModShift)[0]
691 if r.cfg.Animations.On {
692 s = r.settle(t)
693 }
694 return s
695}
696
697// A four-finger swipe down with the overview closed hides the stash, and
698// the next one shows it again with its windows fading in; the stash's
699// windows stay where the swipe found them otherwise.
700func TestFourFingerSwipeDownTogglesStash(t *testing.T) {
701 r := startSwipe(t, nil)
702 if !r.cfg.Animations.On {
703 t.Fatal("animations are off in the rig")
704 }
705 if w := windowOf(t, stashedThird(t, r), 3); w.Hidden {
706 t.Fatalf("setup: the stash is hidden: %+v", w)
707 }
708 r.four(t, 0, 40, 10)
709 if w := windowOf(t, r.frame(t, 15*time.Millisecond), 3); w.Hidden || !(w.Fade > 0 && w.Fade < 1) {
710 t.Fatalf("hiding: window 3 hidden %t, fade %v, want it leaving", w.Hidden, w.Fade)
711 }
712 if w := windowOf(t, r.settle(t), 3); !w.Hidden {
713 t.Fatalf("swipe down left the stash shown: %+v", w)
714 }
715 r.four(t, 0, 40, 10)
716 if w := windowOf(t, r.frame(t, 15*time.Millisecond), 3); w.Hidden || !(w.Fade > 0 && w.Fade < 1) {
717 t.Fatalf("showing: window 3 hidden %t, fade %v, want it appearing", w.Hidden, w.Fade)
718 }
719 if w := windowOf(t, r.settle(t), 3); w.Hidden || w.Fade != 0 {
720 t.Fatalf("second swipe down did not show the stash: %+v", w)
721 }
722}
723
724// A four-finger swipe up hides the shown stash, with the same fade as a
725// swipe down, instead of opening the overview.
726func TestFourFingerSwipeUpHidesShownStash(t *testing.T) {
727 r := startSwipe(t, nil)
728 stashedThird(t, r)
729 s := r.four(t, 0, -40, 10)
730 for _, w := range s.Windows {
731 if w.Preview > 0 {
732 t.Fatal("swipe up opened the overview with the stash shown")
733 }
734 }
735 if w := windowOf(t, r.frame(t, 15*time.Millisecond), 3); w.Hidden || !(w.Fade > 0 && w.Fade < 1) {
736 t.Fatalf("hiding: window 3 hidden %t, fade %v, want it leaving", w.Hidden, w.Fade)
737 }
738 s = r.settle(t)
739 if w := windowOf(t, s, 3); !w.Hidden {
740 t.Fatalf("swipe up left the stash shown: %+v", w)
741 }
742 for _, w := range s.Windows {
743 if w.Preview > 0 {
744 t.Fatal("overview open after the stash hid")
745 }
746 }
747}
748
749// With the stash hidden or empty a swipe up opens the overview.
750func TestFourFingerSwipeUpOpensOverviewWithoutShownStash(t *testing.T) {
751 inOverview := func(s ports.Scene) bool {
752 for _, w := range s.Windows {
753 if w.Preview > 0 {
754 return true
755 }
756 }
757 return false
758 }
759 for _, name := range []string{"hidden", "empty"} {
760 r := startSwipe(t, nil)
761 if name == "hidden" {
762 stashedThird(t, r)
763 r.key(t, "s", ports.ModAlt)
764 r.settle(t)
765 } else {
766 threeColumns(t, r)
767 }
768 r.four(t, 0, -40, 10)
769 if !inOverview(r.settle(t)) {
770 t.Fatalf("%s stash: swipe up did not open the overview", name)
771 }
772 }
773}
774
775// The stash shown over a covering fullscreen window is on screen: a swipe
776// up hides it and leaves the window fullscreen.
777func TestFourFingerSwipeUpHidesStashOverCover(t *testing.T) {
778 r := startSwipe(t, nil)
779 stashedThird(t, r)
780 r.key(t, "s", ports.ModAlt) // hide
781 r.settle(t)
782 r.key(t, "f", ports.ModAlt|ports.ModShift)
783 r.settleAll(t, r.outs...)
784 shown := r.key(t, "s", ports.ModAlt)[0] // show over the cover
785 sc, _ := r.settleAll(t, r.outs...)
786 if len(sc) == 0 {
787 sc = []ports.Scene{shown}
788 }
789 if w := windowOf(t, sc[0], 3); w.Hidden {
790 t.Fatalf("setup: the stash is hidden over the cover: %+v", w)
791 }
792 s := r.four(t, 0, -40, 10)
793 for _, w := range s.Windows {
794 if w.Preview > 0 {
795 t.Fatal("swipe up opened the overview with the stash over the cover")
796 }
797 }
798 // Over a cover the stash hides without a fade: the scene is final.
799 if w := windowOf(t, s, 3); !w.Hidden {
800 t.Fatalf("swipe up left the stash over the cover: %+v", w)
801 }
802 if w := windowOf(t, s, 2); !w.Fullscreen {
803 t.Fatalf("the covering window left fullscreen: %+v", w)
804 }
805}
806
807// With nothing in the stash a swipe down changes nothing: the scene is the
808// same one, with no new Seq and no animation.
809func TestFourFingerSwipeDownEmptyStash(t *testing.T) {
810 r := startSwipe(t, nil)
811 before := threeColumns(t, r)
812 after := r.four(t, 0, 40, 10)
813 if after.Seq != before.Seq || !after.SameAs(before) {
814 t.Fatalf("empty stash: scene changed, seq %d then %d", before.Seq, after.Seq)
815 }
816 if _, ok := r.settleAll(t, r.outs...); ok {
817 t.Fatal("empty stash: the swipe started an animation")
818 }
819}
820
821// With the overview open a swipe down closes it and does not touch the
822// stash, shown or hidden.
823func TestFourFingerSwipeDownInOverviewKeepsStash(t *testing.T) {
824 for _, hidden := range []bool{false, true} {
825 r := startSwipe(t, nil)
826 stashedThird(t, r)
827 if hidden {
828 r.key(t, "s", ports.ModAlt)
829 r.settle(t)
830 }
831 r.four(t, 0, -40, 10)
832 r.settle(t)
833 s := r.four(t, 0, 40, 10)
834 for _, w := range s.Windows {
835 if w.Preview > 0 {
836 t.Fatalf("hidden=%v: swipe down left the overview open", hidden)
837 }
838 }
839 if w := windowOf(t, r.settle(t), 3); w.Hidden != hidden {
840 t.Fatalf("hidden=%v: window 3 hidden %t after the overview closed", hidden, w.Hidden)
841 }
842 }
843}
844
845// Opening the overview during a column swipe drops the swipe: the rest
846// of it neither slides the columns under the overview nor lands them,
847// also once the overview closed again (escape before the rest).
848func TestOverviewOpenedMidSwipeDropsIt(t *testing.T) {
849 for _, closeFirst := range []bool{false, true} {
850 r := startSwipe(t, nil)
851 settled := threeColumns(t, r)
852 r.begin()
853 r.move(t, -40, 0)
854 r.move(t, -40, 0)
855 r.key(t, "o", ports.ModAlt)
856 // s is the latest scene.
857 var s ports.Scene
858 if closeFirst {
859 s = r.key(t, "Escape", 0)[0]
860 }
861 for range 20 {
862 r.at += 8 * time.Millisecond
863 r.input <- ports.SwipeUpdate{DX: -80, Time: r.at}
864 }
865 // The updates publish nothing (the overview gesture, or a dropped
866 // swipe); the lift always publishes, so its scene is received
867 // before the next event: core is idle when the escape and the
868 // flips arrive, each waits for its own scene.
869 r.input <- ports.SwipeEnd{Time: r.at}
870 s = scene(t, r.scenes)
871 if !closeFirst {
872 s = r.key(t, "Escape", 0)[0]
873 }
874 // Past any spring: the escape's own springs (the cards zooming
875 // back) may chain after the swipe's landing, so settle until
876 // none runs.
877 for range 4 {
878 set, ok := r.settleAll(t, wide.Name)
879 if !ok {
880 break
881 }
882 s = set[0]
883 }
884 if len(s.Windows) == 0 || slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool {
885 // Neither a card nor a window still zooming back.
886 return w.Preview > 0 || w.Zoom > 0 || w.Fade > 0
887 }) {
888 t.Fatalf("closeFirst=%v: the scene never settled: %+v", closeFirst, s.Windows)
889 }
890 for _, id := range []ports.WindowID{1, 2, 3} {
891 before, _ := rectOf(settled, id)
892 if after, _ := rectOf(s, id); before != after {
893 t.Fatalf("closeFirst=%v: window %d moved: %+v then %+v", closeFirst, id, before, after)
894 }
895 }
896 }
897}
898
899// A hard flick down from the first of four workspaces lands on the second:
900// one swipe never skips a workspace.
901func TestSwipeHardFlickMovesOneWorkspace(t *testing.T) {
902 r := startSwipe(t, nil)
903 for id := ports.WindowID(1); id <= 4; id++ {
904 r.mapWindow(t, id)
905 r.key(t, "Next", ports.ModAlt)
906 }
907 for range 4 {
908 r.key(t, "Prior", ports.ModAlt)
909 }
910 r.flick(t, 20, 0, 60)
911 s := r.settle(t)
912 if got, ok := rectOf(s, 2); !ok || got.Y != 0 {
913 t.Fatalf("hard flick did not land on workspace 2: %v %t", got, ok)
914 }
915}
916
917// A hard flick across many columns moves the view one column edge.
918func TestSwipeHardFlickMovesOneColumn(t *testing.T) {
919 r := startSwipe(t, nil)
920 for id := ports.WindowID(1); id <= 5; id++ {
921 r.mapWindow(t, id)
922 }
923 // The view shows 4 and 5; a hard flick left shows 3 and 4, not 1.
924 r.flick(t, 20, -60, 0)
925 s := r.settle(t)
926 if got, ok := rectOf(s, 3); !ok || got.X != 0 {
927 t.Fatalf("column 3 at %v %t, want the left edge", got, ok)
928 }
929}
930
931// A two-finger scroll that steps in the overview and ends with the pointer
932// on another output does not hold the next scroll back.
933func TestOverviewScrollStopOnOtherOutput(t *testing.T) {
934 r := startSwipe(t, nil)
935 threeColumns(t, r)
936 r.plug(t, ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000})
937 r.key(t, "o", ports.ModAlt)
938 selected := func(s ports.Scene) ports.WindowID {
939 for _, w := range s.Windows {
940 if w.Focused && w.Preview > 0 {
941 return w.ID
942 }
943 }
944 return 0
945 }
946 on := func(id ports.WindowID) func(ports.Scene) bool {
947 return func(s ports.Scene) bool { return s.Output == wide.Name && selected(s) == id }
948 }
949 finger := func(dx float64) ports.SwipeUpdate {
950 return ports.SwipeUpdate{DX: dx}
951 }
952 r.input <- ports.PointerMotion{X: 100, Y: 300}
953 r.input <- ports.SwipeBegin{Fingers: 3}
954 r.input <- finger(-70)
955 sceneMatch(t, r.scenes, on(2))
956 // The pointer crosses to DP-2 and the fingers lift there.
957 r.input <- ports.PointerMotion{X: 1000, Y: 300}
958 r.input <- ports.SwipeEnd{}
959 r.input <- ports.PointerMotion{X: 100, Y: 300}
960 r.input <- ports.SwipeBegin{Fingers: 3}
961 r.input <- finger(-70)
962 sceneMatch(t, r.scenes, on(1))
963}
964
965// The output shrinks mid-swipe: the snap points no longer hold, so the
966// swipe lets go. The view shows what the resize alone shows, and the rest
967// of the swipe moves nothing.
968func TestSwipeOutputResizedMidSwipe(t *testing.T) {
969 smaller := ports.OutputInfo{Name: wide.Name, Width: 600, Height: 600, RefreshMilli: 60000}
970 four := func(r *swipeRig) {
971 for id := ports.WindowID(1); id <= 4; id++ {
972 r.mapWindow(t, id)
973 }
974 }
975 plain := startSwipe(t, nil)
976 four(plain)
977 plain.output <- ports.OutputAdded{Info: smaller}
978 want := scene(t, plain.scenes)
979
980 r := startSwipe(t, nil)
981 four(r)
982 r.begin()
983 for range 3 {
984 r.move(t, -40, 0)
985 }
986 r.output <- ports.OutputAdded{Info: smaller}
987 scene(t, r.scenes)
988 // The first update after the resize lets go: the slide stops.
989 s := r.move(t, -40, 0)
990 for _, id := range []ports.WindowID{1, 2, 3, 4} {
991 w, _ := rectOf(want, id)
992 if got, _ := rectOf(s, id); got != w {
993 t.Fatalf("window %d at %+v after the next update, want %+v", id, got, w)
994 }
995 }
996 for range 2 {
997 r.at += 8 * time.Millisecond
998 r.input <- ports.SwipeUpdate{DX: -40, Time: r.at}
999 }
1000 s = r.end(t, false)
1001 for _, id := range []ports.WindowID{1, 2, 3, 4} {
1002 w, _ := rectOf(want, id)
1003 if got, _ := rectOf(s, id); got != w {
1004 t.Fatalf("window %d at %+v, want %+v as without the swipe", id, got, w)
1005 }
1006 }
1007}
1008
1009// A window maps mid-swipe: the column snap points moved, so the swipe lets
1010// go and the view shows what the map alone shows.
1011func TestSwipeWindowMappedMidSwipe(t *testing.T) {
1012 plain := startSwipe(t, nil)
1013 threeColumns(t, plain)
1014 want := plain.mapWindow(t, 4)[0]
1015
1016 r := startSwipe(t, nil)
1017 threeColumns(t, r)
1018 r.begin()
1019 for range 3 {
1020 r.move(t, -40, 0)
1021 }
1022 r.mapWindow(t, 4)
1023 r.move(t, -40, 0)
1024 r.at += 8 * time.Millisecond
1025 r.input <- ports.SwipeUpdate{DX: -40, Time: r.at}
1026 s := r.end(t, false)
1027 for _, id := range []ports.WindowID{1, 2, 3, 4} {
1028 w, _ := rectOf(want, id)
1029 if got, _ := rectOf(s, id); got != w {
1030 t.Fatalf("window %d at %+v, want %+v as without the swipe", id, got, w)
1031 }
1032 }
1033}
1034
1035// A press that moves the view during a landing slide redirects the slide
1036// without stopping or reversing it: the view keeps its speed.
1037func TestSwipeRetargetKeepsVelocity(t *testing.T) {
1038 r := startSwipe(t, nil)
1039 for id := ports.WindowID(1); id <= 4; id++ {
1040 r.mapWindow(t, id)
1041 }
1042 // Focus on 4, the view shows 3 and 4. Go back to the first column.
1043 for range 3 {
1044 r.key(t, "Left", ports.ModAlt)
1045 }
1046 r.settle(t)
1047 // A flick toward the right lands on a later column: the view moves
1048 // right, so column 1 moves left.
1049 r.flick(t, 4, 40, 0)
1050 mid := r.frame(t, 16*time.Millisecond)
1051 prev, ok := rectOf(mid, 1)
1052 if !ok {
1053 t.Fatal("column 1 hidden during the landing")
1054 }
1055 if prev.X >= 0 {
1056 t.Fatalf("view did not move right: column 1 at %d", prev.X)
1057 }
1058 r.key(t, "Right", ports.ModAlt)
1059 // The retargeted spring keeps the speed of the landing: the view moves
1060 // on from the first frame and never goes back.
1061 for i := range 2 {
1062 s := r.frame(t, 16*time.Millisecond)
1063 got, _ := rectOf(s, 1)
1064 if got.X > prev.X || (i == 1 && got.X == prev.X) {
1065 t.Fatalf("frame %d: column 1 at %d after %d, the view reversed or stopped", i, got.X, prev.X)
1066 }
1067 prev = got
1068 }
1069}