feat/data-dnd e05cb15

transition_test.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close