1package core_test
2
3import (
4 "context"
5 "sync"
6 "testing"
7 "time"
8
9 "github.com/bnema/neferwl/internal/core"
10 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
11 "github.com/bnema/neferwl/internal/ports"
12 "github.com/stretchr/testify/mock"
13)
14
15// swipeRig runs core on one 800x600 output with no gaps or border, a
16// clock the test moves, and page flips the test sends.
17type swipeRig struct {
18 *multiRig
19 frames chan ports.OutputFrame
20 mu sync.Mutex
21 now time.Time
22 // at is the device time of the next swipe event.
23 at time.Duration
24}
25
26var wide = ports.OutputInfo{Name: "DP-1", Width: 800, Height: 600, RefreshMilli: 60000}
27
28func startSwipe(t *testing.T, edit func(*ports.Config)) *swipeRig {
29 t.Helper()
30 cfg := altCmdDefaults()
31 cfg.Border.Width = 0
32 cfg.Layout.Gaps = 0
33 cfg.Terminal.AutoOpen = "never"
34 if edit != nil {
35 edit(&cfg)
36 }
37 r := &swipeRig{frames: make(chan ports.OutputFrame), now: time.Unix(100, 0)}
38 r.multiRig = &multiRig{
39 client: make(chan ports.ClientEvent, 16), input: make(chan ports.InputEvent, 64),
40 output: make(chan ports.OutputEvent, 4), reload: make(chan ports.ConfigChanged, 4),
41 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,
42 }
43 clock := portsmocks.NewMockClock(t)
44 clock.EXPECT().Now().RunAndReturn(func() time.Time {
45 r.mu.Lock()
46 defer r.mu.Unlock()
47 return r.now
48 }).Maybe()
49 // The fallback timer never fires: frames come from the test.
50 clock.EXPECT().NewTimer(mock.Anything).RunAndReturn(func(time.Duration) ports.Timer {
51 timer := portsmocks.NewMockTimer(t)
52 timer.EXPECT().C().Return(make(chan time.Time)).Maybe()
53 timer.EXPECT().Stop().Return(true).Maybe()
54 return timer
55 }).Maybe()
56 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: clock, Frames: r.frames})
57 if err != nil {
58 t.Fatal(err)
59 }
60 ctx, cancel := context.WithCancel(context.Background())
61 done := make(chan error, 1)
62 go func() { done <- c.Run(ctx) }()
63 t.Cleanup(func() { cancel(); <-done })
64 r.plug(t, wide)
65 return r
66}
67
68func (r *swipeRig) advance(d time.Duration) {
69 r.mu.Lock()
70 r.now = r.now.Add(d)
71 r.mu.Unlock()
72}
73
74// begin starts a swipe; nothing moves until it picks an axis.
75func (r *swipeRig) begin() {
76 r.at += time.Second
77 r.input <- ports.SwipeBegin{Fingers: 3, Time: r.at}
78}
79
80// move sends one swipe update and returns the scene it produced.
81func (r *swipeRig) move(t *testing.T, dx, dy float64) ports.Scene {
82 t.Helper()
83 r.at += 8 * time.Millisecond
84 r.input <- ports.SwipeUpdate{DX: dx, DY: dy, Time: r.at}
85 return scene(t, r.scenes)
86}
87
88func (r *swipeRig) end(t *testing.T, cancelled bool) ports.Scene {
89 t.Helper()
90 r.input <- ports.SwipeEnd{Cancelled: cancelled, Time: r.at}
91 return scene(t, r.scenes)
92}
93
94// frame moves the clock by d, sends a flip and returns the next scene.
95func (r *swipeRig) frame(t *testing.T, d time.Duration) ports.Scene {
96 t.Helper()
97 r.advance(d)
98 r.frames <- ports.OutputFrame{Output: wide.Name}
99 return scene(t, r.scenes)
100}
101
102// settle moves the clock past any spring and returns the settled scene.
103func (r *swipeRig) settle(t *testing.T) ports.Scene {
104 t.Helper()
105 return r.frame(t, 5*time.Second)
106}
107
108// flick sends a quick swipe of n updates of (dx, dy) and lifts the
109// fingers; it returns the scene right after the lift.
110func (r *swipeRig) flick(t *testing.T, n int, dx, dy float64) ports.Scene {
111 t.Helper()
112 r.begin()
113 for range n {
114 r.move(t, dx, dy)
115 }
116 return r.end(t, false)
117}
118
119// workspaces puts window 1 on the first workspace and 2 on the second,
120// showing the first.
121func (r *swipeRig) workspaces(t *testing.T) {
122 t.Helper()
123 r.mapWindow(t, 1)
124 r.key(t, "Next", ports.ModAlt)
125 r.mapWindow(t, 2)
126 r.key(t, "Prior", ports.ModAlt)
127}
128
129func rectOf(s ports.Scene, id ports.WindowID) (ports.Rect, bool) {
130 for _, w := range s.Windows {
131 if w.ID == id && !w.Hidden {
132 return w.Rect, true
133 }
134 }
135 return ports.Rect{}, false
136}
137
138func (r *swipeRig) focused(t *testing.T) ports.WindowID {
139 t.Helper()
140 var id ports.WindowID
141 for len(r.commands) > 0 {
142 if v, ok := (<-r.commands).(ports.FocusWindow); ok {
143 id = v.ID
144 }
145 }
146 return id
147}
148
149// threeColumns maps three windows of half the width; the view shows 2
150// and 3, with 3 focused. It returns the last scene.
151func threeColumns(t *testing.T, r *swipeRig) ports.Scene {
152 t.Helper()
153 var s []ports.Scene
154 for id := ports.WindowID(1); id <= 3; id++ {
155 s = r.mapWindow(t, id)
156 }
157 return s[0]
158}
159
160func TestSwipeFollowsFingers(t *testing.T) {
161 r := startSwipe(t, nil)
162 before, _ := rectOf(threeColumns(t, r), 2)
163 r.begin()
164 // Under the 16 unit threshold the swipe has not picked its axis:
165 // that movement is not applied (as in GNOME Shell and niri).
166 r.input <- ports.SwipeUpdate{DX: -10, Time: r.at}
167 s := r.move(t, -10, 0)
168 after, _ := rectOf(s, 2)
169 // 10 units of 1200 per 800 px view are 7 px; near the resting view
170 // the detent holds the view back a little.
171 if d := after.X - before.X; d <= 0 || d >= 7 {
172 t.Fatalf("column moved %d px with the fingers", d)
173 }
174 // Halfway to the next column edge (400 px: 600 units) the view has
175 // caught up with the fingers; a full step shows whole.
176 s = r.move(t, -290, 0)
177 if got, _ := rectOf(s, 2); got.X-before.X != 200 {
178 t.Fatalf("column moved %d px halfway", got.X-before.X)
179 }
180 s = r.move(t, -300, 0)
181 if got, _ := rectOf(s, 2); got.X-before.X != 400 {
182 t.Fatalf("column moved %d px after a full step", got.X-before.X)
183 }
184}
185
186func TestSwipeFlickLandsOnColumnWithSpring(t *testing.T) {
187 r := startSwipe(t, nil)
188 threeColumns(t, r)
189 // Focus is on 3 (right); the view shows 2 and 3. Flick right to left
190 // fingers (content moves left in classic scroll): view goes left.
191 r.focused(t)
192 s := r.flick(t, 10, -40, 0)
193 mid, ok := rectOf(s, 1)
194 if !ok {
195 t.Fatal("column 1 not shown while the view slides")
196 }
197 if mid.X == 0 {
198 t.Fatal("view jumped: no slide")
199 }
200 s = r.frame(t, 16*time.Millisecond)
201 next, _ := rectOf(s, 1)
202 if next.X <= mid.X || next.X > 0 {
203 t.Fatalf("spring does not approach the target: %d then %d", mid.X, next.X)
204 }
205 s = r.settle(t)
206 if got, _ := rectOf(s, 1); got.X != 0 {
207 t.Fatalf("landed at %d, want column 1 at the left edge", got.X)
208 }
209 if id := r.focused(t); id != 1 && id != 2 {
210 t.Fatalf("focus %d after swiping left", id)
211 }
212}
213
214func TestSwipeNaturalScrollInverts(t *testing.T) {
215 for _, natural := range []bool{false, true} {
216 r := startSwipe(t, func(c *ports.Config) { c.Touchpad.NaturalScroll = natural })
217 threeColumns(t, r)
218 dx := -40.0
219 if natural {
220 dx = 40
221 }
222 r.flick(t, 10, dx, 0)
223 s := r.settle(t)
224 if got, _ := rectOf(s, 1); got.X != 0 {
225 t.Fatalf("natural=%t: column 1 at %d", natural, got.X)
226 }
227 }
228}
229
230func TestSwipeWorkspaceSlides(t *testing.T) {
231 r := startSwipe(t, nil)
232 r.workspaces(t)
233 r.begin()
234 s := r.move(t, 0, 30)
235 one, ok1 := rectOf(s, 1)
236 two, ok2 := rectOf(s, 2)
237 if !ok1 || !ok2 {
238 t.Fatalf("both workspaces show while sliding: %v %v", ok1, ok2)
239 }
240 if one.Y >= 0 || two.Y <= 0 || two.Y-one.Y != 600 {
241 t.Fatalf("slide positions %d %d", one.Y, two.Y)
242 }
243 for range 8 {
244 r.move(t, 0, 30)
245 }
246 s = r.end(t, false)
247 if _, ok := rectOf(s, 2); !ok {
248 t.Fatal("target workspace hidden while landing")
249 }
250 s = r.settle(t)
251 if _, ok := rectOf(s, 1); ok {
252 t.Fatal("old workspace still shown")
253 }
254 if got, _ := rectOf(s, 2); got.Y != 0 {
255 t.Fatalf("workspace 2 at %d", got.Y)
256 }
257}
258
259func TestSwipeWorkspaceCancelReturns(t *testing.T) {
260 r := startSwipe(t, nil)
261 r.workspaces(t)
262 r.begin()
263 for range 10 {
264 r.move(t, 0, 40)
265 }
266 r.end(t, true)
267 s := r.settle(t)
268 if got, ok := rectOf(s, 1); !ok || got.Y != 0 {
269 t.Fatalf("cancelled swipe left workspace 1 at %v %t", got, ok)
270 }
271}
272
273func TestSwipeFixedOverflowRunsFocusAction(t *testing.T) {
274 r := startSwipe(t, func(c *ports.Config) { c.Layout.Overflow = "fixed" })
275 r.mapWindow(t, 1)
276 r.mapWindow(t, 2)
277 r.focused(t)
278 // The view cannot slide: nothing moves until the fingers lift.
279 r.begin()
280 for range 10 {
281 r.at += 8 * time.Millisecond
282 r.input <- ports.SwipeUpdate{DX: -40, Time: r.at}
283 }
284 r.end(t, false)
285 if id := r.focused(t); id != 1 {
286 t.Fatalf("fixed overflow swipe left focuses %d", id)
287 }
288}
289
290func TestSwipeShortSwipeDoesNothingInFixedOverflow(t *testing.T) {
291 r := startSwipe(t, func(c *ports.Config) { c.Layout.Overflow = "fixed" })
292 r.mapWindow(t, 1)
293 r.mapWindow(t, 2)
294 r.focused(t)
295 r.begin()
296 r.input <- ports.SwipeUpdate{DX: -20, Time: r.at + time.Second}
297 r.input <- ports.SwipeEnd{Time: r.at + 2*time.Second}
298 r.key(t, "Return", 0)
299 if id := r.focused(t); id != 0 {
300 t.Fatalf("slow short swipe moved focus to %d", id)
301 }
302}
303
304func TestKeyDuringSlideRetargets(t *testing.T) {
305 r := startSwipe(t, nil)
306 threeColumns(t, r)
307 s := r.flick(t, 10, -40, 0)
308 landing, _ := rectOf(s, 2)
309 // Back to column 3: the view heads there from where it is, no jump.
310 r.key(t, "Right", ports.ModAlt)
311 s = r.key(t, "Right", ports.ModAlt)[0]
312 if got, _ := rectOf(s, 2); got.X != landing.X {
313 t.Fatalf("view jumped from %d to %d", landing.X, got.X)
314 }
315 // The retargeted spring starts on the next frame.
316 r.frame(t, 16*time.Millisecond)
317 s = r.settle(t)
318 if got, _ := rectOf(s, 3); got.X != 400 {
319 t.Fatalf("column 3 landed at %d", got.X)
320 }
321}
322
323func TestSwipeFromEmptyWorkspaceLandsWithoutJump(t *testing.T) {
324 r := startSwipe(t, nil)
325 r.workspaces(t)
326 // Workspace 3 is the trailing empty one; start there and swipe up to 2.
327 r.key(t, "Next", ports.ModAlt)
328 r.key(t, "Next", ports.ModAlt)
329 r.begin()
330 var s ports.Scene
331 for range 6 {
332 s = r.move(t, 0, -40)
333 }
334 before, ok := rectOf(s, 2)
335 if !ok {
336 t.Fatal("workspace 2 not sliding in")
337 }
338 // Landing drops no numbered workspace here (the trailing one stays),
339 // but the view must not jump either way.
340 s = r.end(t, false)
341 after, _ := rectOf(s, 2)
342 if after.Y != before.Y {
343 t.Fatalf("landing jumped from %d to %d", before.Y, after.Y)
344 }
345 if got, _ := rectOf(r.settle(t), 2); got.Y != 0 {
346 t.Fatalf("workspace 2 landed at %d", got.Y)
347 }
348}
349
350func TestSwipeLandingKeepsViewWhenEmptyWorkspaceDrops(t *testing.T) {
351 r := startSwipe(t, nil)
352 // 1 on the first workspace, 2 on the third; the second is empty and
353 // active, so leaving it drops it and renumbers the third.
354 r.mapWindow(t, 1)
355 r.key(t, "Next", ports.ModAlt)
356 r.key(t, "Next", ports.ModAlt)
357 r.mapWindow(t, 2)
358 r.key(t, "Prior", ports.ModAlt)
359 r.begin()
360 var s ports.Scene
361 for range 4 {
362 s = r.move(t, 0, 20)
363 }
364 before, ok := rectOf(s, 2)
365 if !ok {
366 t.Fatal("workspace 3 not sliding in")
367 }
368 s = r.end(t, false)
369 after, ok := rectOf(s, 2)
370 if !ok || after.Y != before.Y {
371 t.Fatalf("landing moved workspace 3 from %d to %d (shown %t)", before.Y, after.Y, ok)
372 }
373 if got, _ := rectOf(r.settle(t), 2); got.Y != 0 {
374 t.Fatalf("landed at %d", got.Y)
375 }
376}
377
378func TestSwipeDropsWhenWorkspacesChange(t *testing.T) {
379 r := startSwipe(t, nil)
380 r.workspaces(t)
381 r.begin()
382 r.move(t, 0, 30)
383 // A key switches workspace mid-swipe: the swipe lets go at once.
384 s := r.key(t, "Next", ports.ModAlt)[0]
385 if got, ok := rectOf(s, 2); !ok || got.Y != 0 {
386 t.Fatalf("workspace 2 at %v %t after the key", got, ok)
387 }
388 if _, ok := rectOf(s, 1); ok {
389 t.Fatal("workspace 1 still sliding")
390 }
391 // The rest of the swipe moves nothing: its end leaves the view as is.
392 r.at += 8 * time.Millisecond
393 r.input <- ports.SwipeUpdate{DY: 30, Time: r.at}
394 if got, ok := rectOf(r.end(t, false), 2); !ok || got.Y != 0 {
395 t.Fatalf("workspace 2 at %v %t after the swipe", got, ok)
396 }
397}
398
399func TestSlideMovesPointerFocus(t *testing.T) {
400 r := startSwipe(t, nil)
401 threeColumns(t, r)
402 // The pointer rests over column 2 (x 100: the view shows 2 and 3).
403 r.input <- ports.PointerMotion{X: 100, Y: 300}
404 r.drain()
405 r.flick(t, 10, -40, 0)
406 r.settle(t)
407 var focus ports.PointerFocus
408 var motion ports.PointerMotionTo
409 // Core sends the pointer update right after the scene: wait for it.
410 for {
411 var v ports.ClientCommand
412 select {
413 case v = <-r.commands:
414 case <-time.After(50 * time.Millisecond):
415 }
416 if v == nil {
417 break
418 }
419 switch v := v.(type) {
420 case ports.PointerFocus:
421 focus = v
422 case ports.PointerMotionTo:
423 motion = v
424 }
425 }
426 // Column 1 slid under the still pointer; it follows the window.
427 if focus.ID != 1 {
428 t.Fatalf("pointer focus %+v", focus)
429 }
430 if motion.ID != 1 || motion.X != 100 || motion.DX != 0 {
431 t.Fatalf("last motion %+v", motion)
432 }
433}
434
435func TestSwipeBeginWithoutEndSettlesTheFirst(t *testing.T) {
436 r := startSwipe(t, nil)
437 threeColumns(t, r)
438 r.begin()
439 s := r.move(t, -40, 0)
440 moved, _ := rectOf(s, 2)
441 // A begin without the previous end: the first swipe springs back.
442 r.begin()
443 scene(t, r.scenes)
444 s = r.settle(t)
445 if got, _ := rectOf(s, 2); got.X == moved.X || got.X != 0 {
446 t.Fatalf("first swipe left column 2 at %d", got.X)
447 }
448}
449
450func TestSwipeIgnoredAfterSwitchingAwayAndBack(t *testing.T) {
451 r := startSwipe(t, nil)
452 before, _ := rectOf(threeColumns(t, r), 2)
453 r.begin()
454 r.move(t, -30, 0)
455 r.key(t, "Next", ports.ModAlt)
456 r.key(t, "Prior", ports.ModAlt)
457 // The same workspace is back, but the swipe was let go.
458 r.at += 8 * time.Millisecond
459 r.input <- ports.SwipeUpdate{DX: -30, Time: r.at}
460 if got, _ := rectOf(r.end(t, false), 2); got != before {
461 t.Fatalf("column 2 at %v, want %v", got, before)
462 }
463}
464
465func TestSwipeKeptWhenAnotherOutputSwitchesWorkspace(t *testing.T) {
466 r := startSwipe(t, nil)
467 before, _ := rectOf(threeColumns(t, r), 2)
468 r.plug(t, ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000})
469 // Window 4 on DP-2, so it has a workspace below to switch to.
470 r.input <- ports.PointerMotion{X: 1000, Y: 300}
471 scene(t, r.scenes)
472 r.mapWindow(t, 4)
473 r.input <- ports.PointerMotion{X: 100, Y: 300}
474 scene(t, r.scenes)
475 r.drain()
476 r.begin()
477 r.move(t, -30, 0)
478 // The pointer goes to DP-2 and a bind switches its workspace.
479 r.input <- ports.PointerMotion{X: 1000, Y: 300}
480 scene(t, r.scenes)
481 for _, sc := range r.key(t, "Next", ports.ModAlt) {
482 if _, ok := rectOf(sc, 4); ok {
483 t.Fatal("DP-2 did not switch workspace")
484 }
485 }
486 // The swipe on DP-1 still follows the fingers.
487 var got ports.Rect
488 for _, sc := range receiveMove(t, r, -570) {
489 if sc.Output == wide.Name {
490 got, _ = rectOf(sc, 2)
491 }
492 }
493 // 600 units at 2/3 px each: a full step of 400 px.
494 if got.X-before.X != 400 {
495 t.Fatalf("column 2 moved %d px", got.X-before.X)
496 }
497}
498
499// receiveMove sends one swipe update and returns every output's scene.
500func receiveMove(t *testing.T, r *swipeRig, dx float64) []ports.Scene {
501 t.Helper()
502 r.at += 8 * time.Millisecond
503 r.input <- ports.SwipeUpdate{DX: dx, Time: r.at}
504 return receive(t, r.scenes)
505}
506
507func TestDiscreteSwipeSkippedWhenPointerChangesOutput(t *testing.T) {
508 r := startSwipe(t, func(c *ports.Config) { c.Layout.Overflow = "fixed" })
509 r.mapWindow(t, 1)
510 r.mapWindow(t, 2)
511 r.plug(t, ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000})
512 // Windows 3 and 4 on DP-2, 4 focused there.
513 r.input <- ports.PointerMotion{X: 1000, Y: 300}
514 scene(t, r.scenes)
515 r.mapWindow(t, 3)
516 r.mapWindow(t, 4)
517 // The swipe starts on DP-1 ...
518 r.input <- ports.PointerMotion{X: 100, Y: 300}
519 scene(t, r.scenes)
520 r.begin()
521 for range 10 {
522 r.at += 8 * time.Millisecond
523 r.input <- ports.SwipeUpdate{DX: -40, Time: r.at}
524 }
525 // ... and the pointer takes the focus to DP-2 before the fingers lift.
526 r.input <- ports.PointerMotion{X: 1000, Y: 300}
527 scene(t, r.scenes)
528 r.drain()
529 r.input <- ports.SwipeEnd{Time: r.at}
530 for {
531 var v ports.ClientCommand
532 select {
533 case v = <-r.commands:
534 case <-time.After(50 * time.Millisecond):
535 }
536 if v == nil {
537 break
538 }
539 if f, ok := v.(ports.FocusWindow); ok && f.ID == 3 {
540 t.Fatal("swipe from DP-1 moved the focus on DP-2")
541 }
542 }
543}
544
545// drain waits for core to handle what was sent, then empties commands.
546func (r *swipeRig) drain() {
547 for {
548 select {
549 case <-r.commands:
550 case <-time.After(50 * time.Millisecond):
551 return
552 }
553 }
554}
555
556func TestKeyWithoutSlideMovesAtOnce(t *testing.T) {
557 r := startSwipe(t, nil)
558 threeColumns(t, r)
559 r.key(t, "Left", ports.ModAlt)
560 s := r.key(t, "Left", ports.ModAlt)[0]
561 if got, _ := rectOf(s, 1); got.X != 0 {
562 t.Fatalf("column 1 at %d", got.X)
563 }
564}
565
566// In the overview a three-finger swipe does not slide the view: nothing
567// is drawn while the fingers move, and when they lift the selection moves
568// one column, the previews where they were.
569func TestSwipeInOverview(t *testing.T) {
570 r := startSwipe(t, nil)
571 threeColumns(t, r)
572 opened := r.key(t, "o", ports.ModAlt)[0]
573 selected := func(s ports.Scene) ports.WindowID {
574 for _, w := range s.Windows {
575 if w.Focused && w.Preview > 0 {
576 return w.ID
577 }
578 }
579 return 0
580 }
581 r.begin()
582 for range 3 {
583 r.at += 8 * time.Millisecond
584 r.input <- ports.SwipeUpdate{DX: -40, Time: r.at}
585 }
586 s := r.end(t, false)
587 if id := selected(s); id != 2 {
588 t.Fatalf("selected %d after a left swipe, want 2", id)
589 }
590 before, _ := rectOf(opened, 2)
591 if after, _ := rectOf(s, 2); before != after {
592 t.Fatalf("preview moved: %+v then %+v", before, after)
593 }
594 // A small swipe does nothing: the l after it moves from 2 to 3.
595 r.begin()
596 r.at += 8 * time.Millisecond
597 r.input <- ports.SwipeUpdate{DX: -20, Time: r.at}
598 if id := selected(r.end(t, false)); id != 2 {
599 t.Fatalf("selected %d after a small swipe", id)
600 }
601 if id := selected(r.key(t, "l", 0)[0]); id != 3 {
602 t.Fatalf("selected %d after a small swipe and l, want 3", id)
603 }
604}
605
606// A four-finger swipe up opens the overview and down closes it on the
607// selection, whatever natural-scroll says (as niri). A short one or one
608// sideways does nothing.
609func TestFourFingerSwipeOverview(t *testing.T) {
610 for _, natural := range []bool{false, true} {
611 r := startSwipe(t, func(c *ports.Config) { c.Touchpad.NaturalScroll = natural })
612 threeColumns(t, r)
613 four := func(dx, dy float64, n int) ports.Scene {
614 r.at += time.Second
615 r.input <- ports.SwipeBegin{Fingers: 4, Time: r.at}
616 for range n {
617 r.at += 8 * time.Millisecond
618 r.input <- ports.SwipeUpdate{DX: dx, DY: dy, Time: r.at}
619 }
620 return r.end(t, false)
621 }
622 inOverview := func(s ports.Scene) bool {
623 for _, w := range s.Windows {
624 if w.Preview > 0 {
625 return true
626 }
627 }
628 return false
629 }
630 if inOverview(four(0, -5, 4)) {
631 t.Fatalf("natural=%v: short swipe opened the overview", natural)
632 }
633 if inOverview(four(-40, 0, 10)) {
634 t.Fatalf("natural=%v: sideways swipe opened the overview", natural)
635 }
636 if !inOverview(four(0, -40, 10)) {
637 t.Fatalf("natural=%v: swipe up did not open the overview", natural)
638 }
639 if !inOverview(four(0, -40, 10)) {
640 t.Fatalf("natural=%v: a second swipe up closed it", natural)
641 }
642 r.key(t, "h", 0)
643 s := four(0, 40, 10)
644 if inOverview(s) {
645 t.Fatalf("natural=%v: swipe down did not close the overview", natural)
646 }
647 if id := r.focused(t); id != 2 {
648 t.Fatalf("natural=%v: closed on %d, want the selection 2", natural, id)
649 }
650 }
651}
652
653// Opening the overview during a column swipe drops the swipe: the rest
654// of it neither slides the columns under the overview nor lands them,
655// also once the overview closed again (escape before the rest).
656func TestOverviewOpenedMidSwipeDropsIt(t *testing.T) {
657 for _, closeFirst := range []bool{false, true} {
658 r := startSwipe(t, nil)
659 settled := threeColumns(t, r)
660 r.begin()
661 r.move(t, -40, 0)
662 r.move(t, -40, 0)
663 r.key(t, "o", ports.ModAlt)
664 if closeFirst {
665 r.key(t, "Escape", 0)
666 }
667 for range 20 {
668 r.at += 8 * time.Millisecond
669 r.input <- ports.SwipeUpdate{DX: -80, Time: r.at}
670 }
671 r.input <- ports.SwipeEnd{Time: r.at}
672 if !closeFirst {
673 r.input <- ports.KeyEvent{Keysym: "Escape", Pressed: true}
674 }
675 // Past any spring: the view shows where it settled.
676 r.advance(5 * time.Second)
677 r.frames <- ports.OutputFrame{Output: wide.Name}
678 s := sceneMatch(t, r.scenes, func(s ports.Scene) bool {
679 for _, w := range s.Windows {
680 if w.Preview > 0 {
681 return false
682 }
683 }
684 return true
685 })
686 for _, id := range []ports.WindowID{1, 2, 3} {
687 before, _ := rectOf(settled, id)
688 if after, _ := rectOf(s, id); before != after {
689 t.Fatalf("closeFirst=%v: window %d moved: %+v then %+v", closeFirst, id, before, after)
690 }
691 }
692 }
693}
694
695// A hard flick down from the first of four workspaces lands on the second:
696// one swipe never skips a workspace.
697func TestSwipeHardFlickMovesOneWorkspace(t *testing.T) {
698 r := startSwipe(t, nil)
699 for id := ports.WindowID(1); id <= 4; id++ {
700 r.mapWindow(t, id)
701 r.key(t, "Next", ports.ModAlt)
702 }
703 for range 4 {
704 r.key(t, "Prior", ports.ModAlt)
705 }
706 r.flick(t, 20, 0, 60)
707 s := r.settle(t)
708 if got, ok := rectOf(s, 2); !ok || got.Y != 0 {
709 t.Fatalf("hard flick did not land on workspace 2: %v %t", got, ok)
710 }
711}
712
713// A hard flick across many columns moves the view one column edge.
714func TestSwipeHardFlickMovesOneColumn(t *testing.T) {
715 r := startSwipe(t, nil)
716 for id := ports.WindowID(1); id <= 5; id++ {
717 r.mapWindow(t, id)
718 }
719 // The view shows 4 and 5; a hard flick left shows 3 and 4, not 1.
720 r.flick(t, 20, -60, 0)
721 s := r.settle(t)
722 if got, ok := rectOf(s, 3); !ok || got.X != 0 {
723 t.Fatalf("column 3 at %v %t, want the left edge", got, ok)
724 }
725}
726
727// A two-finger scroll that steps in the overview and ends with the pointer
728// on another output does not hold the next scroll back.
729func TestOverviewScrollStopOnOtherOutput(t *testing.T) {
730 r := startSwipe(t, nil)
731 threeColumns(t, r)
732 r.plug(t, ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000})
733 r.key(t, "o", ports.ModAlt)
734 selected := func(s ports.Scene) ports.WindowID {
735 for _, w := range s.Windows {
736 if w.Focused && w.Preview > 0 {
737 return w.ID
738 }
739 }
740 return 0
741 }
742 on := func(id ports.WindowID) func(ports.Scene) bool {
743 return func(s ports.Scene) bool { return s.Output == wide.Name && selected(s) == id }
744 }
745 finger := func(dx float64) ports.PointerAxis {
746 return ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: dx}}
747 }
748 r.input <- ports.PointerMotion{X: 100, Y: 300}
749 r.input <- finger(-70)
750 sceneMatch(t, r.scenes, on(2))
751 // The pointer crosses to DP-2 and the fingers lift there.
752 r.input <- ports.PointerMotion{X: 1000, Y: 300}
753 r.input <- ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}}
754 r.input <- ports.PointerMotion{X: 100, Y: 300}
755 r.input <- finger(-70)
756 sceneMatch(t, r.scenes, on(1))
757}
758
759// The output shrinks mid-swipe: the snap points no longer hold, so the
760// swipe lets go. The view shows what the resize alone shows, and the rest
761// of the swipe moves nothing.
762func TestSwipeOutputResizedMidSwipe(t *testing.T) {
763 smaller := ports.OutputInfo{Name: wide.Name, Width: 600, Height: 600, RefreshMilli: 60000}
764 four := func(r *swipeRig) {
765 for id := ports.WindowID(1); id <= 4; id++ {
766 r.mapWindow(t, id)
767 }
768 }
769 plain := startSwipe(t, nil)
770 four(plain)
771 plain.output <- ports.OutputAdded{Info: smaller}
772 want := scene(t, plain.scenes)
773
774 r := startSwipe(t, nil)
775 four(r)
776 r.begin()
777 for range 3 {
778 r.move(t, -40, 0)
779 }
780 r.output <- ports.OutputAdded{Info: smaller}
781 scene(t, r.scenes)
782 // The first update after the resize lets go: the slide stops.
783 s := r.move(t, -40, 0)
784 for _, id := range []ports.WindowID{1, 2, 3, 4} {
785 w, _ := rectOf(want, id)
786 if got, _ := rectOf(s, id); got != w {
787 t.Fatalf("window %d at %+v after the next update, want %+v", id, got, w)
788 }
789 }
790 for range 2 {
791 r.at += 8 * time.Millisecond
792 r.input <- ports.SwipeUpdate{DX: -40, Time: r.at}
793 }
794 s = r.end(t, false)
795 for _, id := range []ports.WindowID{1, 2, 3, 4} {
796 w, _ := rectOf(want, id)
797 if got, _ := rectOf(s, id); got != w {
798 t.Fatalf("window %d at %+v, want %+v as without the swipe", id, got, w)
799 }
800 }
801}
802
803// A window maps mid-swipe: the column snap points moved, so the swipe lets
804// go and the view shows what the map alone shows.
805func TestSwipeWindowMappedMidSwipe(t *testing.T) {
806 plain := startSwipe(t, nil)
807 threeColumns(t, plain)
808 want := plain.mapWindow(t, 4)[0]
809
810 r := startSwipe(t, nil)
811 threeColumns(t, r)
812 r.begin()
813 for range 3 {
814 r.move(t, -40, 0)
815 }
816 r.mapWindow(t, 4)
817 r.move(t, -40, 0)
818 r.at += 8 * time.Millisecond
819 r.input <- ports.SwipeUpdate{DX: -40, Time: r.at}
820 s := r.end(t, false)
821 for _, id := range []ports.WindowID{1, 2, 3, 4} {
822 w, _ := rectOf(want, id)
823 if got, _ := rectOf(s, id); got != w {
824 t.Fatalf("window %d at %+v, want %+v as without the swipe", id, got, w)
825 }
826 }
827}