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, Clock: r.clk.clock, Frames: r.frames})
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 s := r.end(t, false)
605 if id := selected(s); id != 2 {
606 t.Fatalf("selected %d after a left swipe, want 2", id)
607 }
608 before, _ := rectOf(opened, 2)
609 if after, _ := rectOf(s, 2); before != after {
610 t.Fatalf("preview moved: %+v then %+v", before, after)
611 }
612 // A small swipe does nothing: the l after it moves from 2 to 3.
613 r.begin()
614 r.at += 8 * time.Millisecond
615 r.input <- ports.SwipeUpdate{DX: -20, Time: r.at}
616 if id := selected(r.end(t, false)); id != 2 {
617 t.Fatalf("selected %d after a small swipe", id)
618 }
619 if id := selected(r.key(t, "l", 0)[0]); id != 3 {
620 t.Fatalf("selected %d after a small swipe and l, want 3", id)
621 }
622}
623
624// A four-finger swipe up opens the overview and down closes it on the
625// selection, whatever natural-scroll says (as niri). A short one or one
626// sideways does nothing.
627func TestFourFingerSwipeOverview(t *testing.T) {
628 for _, natural := range []bool{false, true} {
629 r := startSwipe(t, func(c *ports.Config) { c.Touchpad.NaturalScroll = natural })
630 threeColumns(t, r)
631 four := func(dx, dy float64, n int) ports.Scene {
632 r.at += time.Second
633 r.input <- ports.SwipeBegin{Fingers: 4, Time: r.at}
634 for range n {
635 r.at += 8 * time.Millisecond
636 r.input <- ports.SwipeUpdate{DX: dx, DY: dy, Time: r.at}
637 }
638 return r.end(t, false)
639 }
640 inOverview := func(s ports.Scene) bool {
641 for _, w := range s.Windows {
642 if w.Preview > 0 {
643 return true
644 }
645 }
646 return false
647 }
648 if inOverview(four(0, -5, 4)) {
649 t.Fatalf("natural=%v: short swipe opened the overview", natural)
650 }
651 if inOverview(four(-40, 0, 10)) {
652 t.Fatalf("natural=%v: sideways swipe opened the overview", natural)
653 }
654 if !inOverview(four(0, -40, 10)) {
655 t.Fatalf("natural=%v: swipe up did not open the overview", natural)
656 }
657 if !inOverview(four(0, -40, 10)) {
658 t.Fatalf("natural=%v: a second swipe up closed it", natural)
659 }
660 r.key(t, "h", 0)
661 s := four(0, 40, 10)
662 if inOverview(s) {
663 t.Fatalf("natural=%v: swipe down did not close the overview", natural)
664 }
665 if id := r.focused(t); id != 2 {
666 t.Fatalf("natural=%v: closed on %d, want the selection 2", natural, id)
667 }
668 }
669}
670
671// four lifts a four-finger swipe of n updates of (dx, dy) after the
672// fingers went down, and returns the scene right after the lift.
673func (r *swipeRig) four(t *testing.T, dx, dy float64, n int) ports.Scene {
674 t.Helper()
675 r.at += time.Second
676 r.input <- ports.SwipeBegin{Fingers: 4, Time: r.at}
677 for range n {
678 r.at += 8 * time.Millisecond
679 r.input <- ports.SwipeUpdate{DX: dx, DY: dy, Time: r.at}
680 }
681 return r.end(t, false)
682}
683
684// stashedThird maps three windows and stashes the third (shown, focused),
685// settled.
686func stashedThird(t *testing.T, r *swipeRig) ports.Scene {
687 t.Helper()
688 threeColumns(t, r)
689 s := r.key(t, "s", ports.ModAlt|ports.ModShift)[0]
690 if r.cfg.Animations.On {
691 s = r.settle(t)
692 }
693 return s
694}
695
696// A four-finger swipe down with the overview closed hides the stash, and
697// the next one shows it again with its windows fading in; the stash's
698// windows stay where the swipe found them otherwise.
699func TestFourFingerSwipeDownTogglesStash(t *testing.T) {
700 r := startSwipe(t, nil)
701 if !r.cfg.Animations.On {
702 t.Fatal("animations are off in the rig")
703 }
704 if w := windowOf(t, stashedThird(t, r), 3); w.Hidden {
705 t.Fatalf("setup: the stash is hidden: %+v", w)
706 }
707 r.four(t, 0, 40, 10)
708 if w := windowOf(t, r.frame(t, 15*time.Millisecond), 3); w.Hidden || !(w.Fade > 0 && w.Fade < 1) {
709 t.Fatalf("hiding: window 3 hidden %t, fade %v, want it leaving", w.Hidden, w.Fade)
710 }
711 if w := windowOf(t, r.settle(t), 3); !w.Hidden {
712 t.Fatalf("swipe down left the stash shown: %+v", w)
713 }
714 r.four(t, 0, 40, 10)
715 if w := windowOf(t, r.frame(t, 15*time.Millisecond), 3); w.Hidden || !(w.Fade > 0 && w.Fade < 1) {
716 t.Fatalf("showing: window 3 hidden %t, fade %v, want it appearing", w.Hidden, w.Fade)
717 }
718 if w := windowOf(t, r.settle(t), 3); w.Hidden || w.Fade != 0 {
719 t.Fatalf("second swipe down did not show the stash: %+v", w)
720 }
721}
722
723// A four-finger swipe up hides the shown stash, with the same fade as a
724// swipe down, instead of opening the overview.
725func TestFourFingerSwipeUpHidesShownStash(t *testing.T) {
726 r := startSwipe(t, nil)
727 stashedThird(t, r)
728 s := r.four(t, 0, -40, 10)
729 for _, w := range s.Windows {
730 if w.Preview > 0 {
731 t.Fatal("swipe up opened the overview with the stash shown")
732 }
733 }
734 if w := windowOf(t, r.frame(t, 15*time.Millisecond), 3); w.Hidden || !(w.Fade > 0 && w.Fade < 1) {
735 t.Fatalf("hiding: window 3 hidden %t, fade %v, want it leaving", w.Hidden, w.Fade)
736 }
737 s = r.settle(t)
738 if w := windowOf(t, s, 3); !w.Hidden {
739 t.Fatalf("swipe up left the stash shown: %+v", w)
740 }
741 for _, w := range s.Windows {
742 if w.Preview > 0 {
743 t.Fatal("overview open after the stash hid")
744 }
745 }
746}
747
748// With the stash hidden or empty a swipe up opens the overview.
749func TestFourFingerSwipeUpOpensOverviewWithoutShownStash(t *testing.T) {
750 inOverview := func(s ports.Scene) bool {
751 for _, w := range s.Windows {
752 if w.Preview > 0 {
753 return true
754 }
755 }
756 return false
757 }
758 for _, name := range []string{"hidden", "empty"} {
759 r := startSwipe(t, nil)
760 if name == "hidden" {
761 stashedThird(t, r)
762 r.key(t, "s", ports.ModAlt)
763 r.settle(t)
764 } else {
765 threeColumns(t, r)
766 }
767 r.four(t, 0, -40, 10)
768 if !inOverview(r.settle(t)) {
769 t.Fatalf("%s stash: swipe up did not open the overview", name)
770 }
771 }
772}
773
774// The stash shown over a covering fullscreen window is on screen: a swipe
775// up hides it and leaves the window fullscreen.
776func TestFourFingerSwipeUpHidesStashOverCover(t *testing.T) {
777 r := startSwipe(t, nil)
778 stashedThird(t, r)
779 r.key(t, "s", ports.ModAlt) // hide
780 r.settle(t)
781 r.key(t, "f", ports.ModAlt|ports.ModShift)
782 r.settleAll(t, r.outs...)
783 shown := r.key(t, "s", ports.ModAlt)[0] // show over the cover
784 sc, _ := r.settleAll(t, r.outs...)
785 if len(sc) == 0 {
786 sc = []ports.Scene{shown}
787 }
788 if w := windowOf(t, sc[0], 3); w.Hidden {
789 t.Fatalf("setup: the stash is hidden over the cover: %+v", w)
790 }
791 s := r.four(t, 0, -40, 10)
792 for _, w := range s.Windows {
793 if w.Preview > 0 {
794 t.Fatal("swipe up opened the overview with the stash over the cover")
795 }
796 }
797 // Over a cover the stash hides without a fade: the scene is final.
798 if w := windowOf(t, s, 3); !w.Hidden {
799 t.Fatalf("swipe up left the stash over the cover: %+v", w)
800 }
801 if w := windowOf(t, s, 2); !w.Fullscreen {
802 t.Fatalf("the covering window left fullscreen: %+v", w)
803 }
804}
805
806// With nothing in the stash a swipe down changes nothing: the scene is the
807// same one, with no new Seq and no animation.
808func TestFourFingerSwipeDownEmptyStash(t *testing.T) {
809 r := startSwipe(t, nil)
810 before := threeColumns(t, r)
811 after := r.four(t, 0, 40, 10)
812 if after.Seq != before.Seq || !after.SameAs(before) {
813 t.Fatalf("empty stash: scene changed, seq %d then %d", before.Seq, after.Seq)
814 }
815 if _, ok := r.settleAll(t, r.outs...); ok {
816 t.Fatal("empty stash: the swipe started an animation")
817 }
818}
819
820// With the overview open a swipe down closes it and does not touch the
821// stash, shown or hidden.
822func TestFourFingerSwipeDownInOverviewKeepsStash(t *testing.T) {
823 for _, hidden := range []bool{false, true} {
824 r := startSwipe(t, nil)
825 stashedThird(t, r)
826 if hidden {
827 r.key(t, "s", ports.ModAlt)
828 r.settle(t)
829 }
830 r.four(t, 0, -40, 10)
831 r.settle(t)
832 s := r.four(t, 0, 40, 10)
833 for _, w := range s.Windows {
834 if w.Preview > 0 {
835 t.Fatalf("hidden=%v: swipe down left the overview open", hidden)
836 }
837 }
838 if w := windowOf(t, r.settle(t), 3); w.Hidden != hidden {
839 t.Fatalf("hidden=%v: window 3 hidden %t after the overview closed", hidden, w.Hidden)
840 }
841 }
842}
843
844// Opening the overview during a column swipe drops the swipe: the rest
845// of it neither slides the columns under the overview nor lands them,
846// also once the overview closed again (escape before the rest).
847func TestOverviewOpenedMidSwipeDropsIt(t *testing.T) {
848 for _, closeFirst := range []bool{false, true} {
849 r := startSwipe(t, nil)
850 settled := threeColumns(t, r)
851 r.begin()
852 r.move(t, -40, 0)
853 r.move(t, -40, 0)
854 r.key(t, "o", ports.ModAlt)
855 // s is the latest scene.
856 var s ports.Scene
857 if closeFirst {
858 s = r.key(t, "Escape", 0)[0]
859 }
860 for range 20 {
861 r.at += 8 * time.Millisecond
862 r.input <- ports.SwipeUpdate{DX: -80, Time: r.at}
863 }
864 // The updates publish nothing (the overview gesture, or a dropped
865 // swipe); the lift always publishes, so its scene is received
866 // before the next event: core is idle when the escape and the
867 // flips arrive, each waits for its own scene.
868 r.input <- ports.SwipeEnd{Time: r.at}
869 s = scene(t, r.scenes)
870 if !closeFirst {
871 s = r.key(t, "Escape", 0)[0]
872 }
873 // Past any spring: the escape's own springs (the cards zooming
874 // back) may chain after the swipe's landing, so settle until
875 // none runs.
876 for range 4 {
877 set, ok := r.settleAll(t, wide.Name)
878 if !ok {
879 break
880 }
881 s = set[0]
882 }
883 if len(s.Windows) == 0 || slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool {
884 // Neither a card nor a window still zooming back.
885 return w.Preview > 0 || w.Zoom > 0 || w.Fade > 0
886 }) {
887 t.Fatalf("closeFirst=%v: the scene never settled: %+v", closeFirst, s.Windows)
888 }
889 for _, id := range []ports.WindowID{1, 2, 3} {
890 before, _ := rectOf(settled, id)
891 if after, _ := rectOf(s, id); before != after {
892 t.Fatalf("closeFirst=%v: window %d moved: %+v then %+v", closeFirst, id, before, after)
893 }
894 }
895 }
896}
897
898// A hard flick down from the first of four workspaces lands on the second:
899// one swipe never skips a workspace.
900func TestSwipeHardFlickMovesOneWorkspace(t *testing.T) {
901 r := startSwipe(t, nil)
902 for id := ports.WindowID(1); id <= 4; id++ {
903 r.mapWindow(t, id)
904 r.key(t, "Next", ports.ModAlt)
905 }
906 for range 4 {
907 r.key(t, "Prior", ports.ModAlt)
908 }
909 r.flick(t, 20, 0, 60)
910 s := r.settle(t)
911 if got, ok := rectOf(s, 2); !ok || got.Y != 0 {
912 t.Fatalf("hard flick did not land on workspace 2: %v %t", got, ok)
913 }
914}
915
916// A hard flick across many columns moves the view one column edge.
917func TestSwipeHardFlickMovesOneColumn(t *testing.T) {
918 r := startSwipe(t, nil)
919 for id := ports.WindowID(1); id <= 5; id++ {
920 r.mapWindow(t, id)
921 }
922 // The view shows 4 and 5; a hard flick left shows 3 and 4, not 1.
923 r.flick(t, 20, -60, 0)
924 s := r.settle(t)
925 if got, ok := rectOf(s, 3); !ok || got.X != 0 {
926 t.Fatalf("column 3 at %v %t, want the left edge", got, ok)
927 }
928}
929
930// A two-finger scroll that steps in the overview and ends with the pointer
931// on another output does not hold the next scroll back.
932func TestOverviewScrollStopOnOtherOutput(t *testing.T) {
933 r := startSwipe(t, nil)
934 threeColumns(t, r)
935 r.plug(t, ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000})
936 r.key(t, "o", ports.ModAlt)
937 selected := func(s ports.Scene) ports.WindowID {
938 for _, w := range s.Windows {
939 if w.Focused && w.Preview > 0 {
940 return w.ID
941 }
942 }
943 return 0
944 }
945 on := func(id ports.WindowID) func(ports.Scene) bool {
946 return func(s ports.Scene) bool { return s.Output == wide.Name && selected(s) == id }
947 }
948 finger := func(dx float64) ports.PointerAxis {
949 return ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: dx}}
950 }
951 r.input <- ports.PointerMotion{X: 100, Y: 300}
952 r.input <- finger(-70)
953 sceneMatch(t, r.scenes, on(2))
954 // The pointer crosses to DP-2 and the fingers lift there.
955 r.input <- ports.PointerMotion{X: 1000, Y: 300}
956 r.input <- ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}}
957 r.input <- ports.PointerMotion{X: 100, Y: 300}
958 r.input <- finger(-70)
959 sceneMatch(t, r.scenes, on(1))
960}
961
962// The output shrinks mid-swipe: the snap points no longer hold, so the
963// swipe lets go. The view shows what the resize alone shows, and the rest
964// of the swipe moves nothing.
965func TestSwipeOutputResizedMidSwipe(t *testing.T) {
966 smaller := ports.OutputInfo{Name: wide.Name, Width: 600, Height: 600, RefreshMilli: 60000}
967 four := func(r *swipeRig) {
968 for id := ports.WindowID(1); id <= 4; id++ {
969 r.mapWindow(t, id)
970 }
971 }
972 plain := startSwipe(t, nil)
973 four(plain)
974 plain.output <- ports.OutputAdded{Info: smaller}
975 want := scene(t, plain.scenes)
976
977 r := startSwipe(t, nil)
978 four(r)
979 r.begin()
980 for range 3 {
981 r.move(t, -40, 0)
982 }
983 r.output <- ports.OutputAdded{Info: smaller}
984 scene(t, r.scenes)
985 // The first update after the resize lets go: the slide stops.
986 s := r.move(t, -40, 0)
987 for _, id := range []ports.WindowID{1, 2, 3, 4} {
988 w, _ := rectOf(want, id)
989 if got, _ := rectOf(s, id); got != w {
990 t.Fatalf("window %d at %+v after the next update, want %+v", id, got, w)
991 }
992 }
993 for range 2 {
994 r.at += 8 * time.Millisecond
995 r.input <- ports.SwipeUpdate{DX: -40, Time: r.at}
996 }
997 s = r.end(t, false)
998 for _, id := range []ports.WindowID{1, 2, 3, 4} {
999 w, _ := rectOf(want, id)
1000 if got, _ := rectOf(s, id); got != w {
1001 t.Fatalf("window %d at %+v, want %+v as without the swipe", id, got, w)
1002 }
1003 }
1004}
1005
1006// A window maps mid-swipe: the column snap points moved, so the swipe lets
1007// go and the view shows what the map alone shows.
1008func TestSwipeWindowMappedMidSwipe(t *testing.T) {
1009 plain := startSwipe(t, nil)
1010 threeColumns(t, plain)
1011 want := plain.mapWindow(t, 4)[0]
1012
1013 r := startSwipe(t, nil)
1014 threeColumns(t, r)
1015 r.begin()
1016 for range 3 {
1017 r.move(t, -40, 0)
1018 }
1019 r.mapWindow(t, 4)
1020 r.move(t, -40, 0)
1021 r.at += 8 * time.Millisecond
1022 r.input <- ports.SwipeUpdate{DX: -40, Time: r.at}
1023 s := r.end(t, false)
1024 for _, id := range []ports.WindowID{1, 2, 3, 4} {
1025 w, _ := rectOf(want, id)
1026 if got, _ := rectOf(s, id); got != w {
1027 t.Fatalf("window %d at %+v, want %+v as without the swipe", id, got, w)
1028 }
1029 }
1030}
1031
1032// A press that moves the view during a landing slide redirects the slide
1033// without stopping or reversing it: the view keeps its speed.
1034func TestSwipeRetargetKeepsVelocity(t *testing.T) {
1035 r := startSwipe(t, nil)
1036 for id := ports.WindowID(1); id <= 4; id++ {
1037 r.mapWindow(t, id)
1038 }
1039 // Focus on 4, the view shows 3 and 4. Go back to the first column.
1040 for range 3 {
1041 r.key(t, "Left", ports.ModAlt)
1042 }
1043 r.settle(t)
1044 // A flick toward the right lands on a later column: the view moves
1045 // right, so column 1 moves left.
1046 r.flick(t, 4, 40, 0)
1047 mid := r.frame(t, 16*time.Millisecond)
1048 prev, ok := rectOf(mid, 1)
1049 if !ok {
1050 t.Fatal("column 1 hidden during the landing")
1051 }
1052 if prev.X >= 0 {
1053 t.Fatalf("view did not move right: column 1 at %d", prev.X)
1054 }
1055 r.key(t, "Right", ports.ModAlt)
1056 // The retargeted spring keeps the speed of the landing: the view moves
1057 // on from the first frame and never goes back.
1058 for i := range 2 {
1059 s := r.frame(t, 16*time.Millisecond)
1060 got, _ := rectOf(s, 1)
1061 if got.X > prev.X || (i == 1 && got.X == prev.X) {
1062 t.Fatalf("frame %d: column 1 at %d after %d, the view reversed or stopped", i, got.X, prev.X)
1063 }
1064 prev = got
1065 }
1066}