v0.4.0 b6be3a8

workspace_size_test.go RawHistoryFolder

1package core_test
2
3import (
4	"testing"
5
6	"github.com/bnema/neferwl/internal/ports"
7)
8
9var ultrawide = ports.OutputInfo{Name: "DP-3", Make: "Acme", Model: "W", Serial: "3", Width: 3440, Height: 1440}
10
11// sizedConfig is the config of the sized-workspace rigs: gaps off, two
12// columns, the named workspace "focus" (1920x1080) bound to Alt+w.
13func sizedConfig(overflow string, edit func(*ports.Config)) func(*ports.Config) {
14	return func(c *ports.Config) {
15		c.Layout.Gaps = 0
16		c.Layout.MaxColumns = 2
17		c.Workspaces = []ports.WorkspaceConfig{{Name: "focus", Size: [2]int{1920, 1080}, LayoutRules: ports.LayoutRules{Overflow: overflow}}}
18		c.Binds["Alt+w"] = "workspace focus"
19		if edit != nil {
20			edit(c)
21		}
22	}
23}
24
25// sizedRig runs an ultrawide with the named workspace "focus" (1920x1080)
26// bound to Alt+w, and shows it. The animations setting is the default's
27// unless edit changes it.
28func sizedRig(t *testing.T, overflow string, edit func(*ports.Config)) (*multiRig, ports.Scene) {
29	t.Helper()
30	r := startMulti(t, sizedConfig(overflow, edit), ultrawide)
31	return r, r.key(t, "w", ports.ModAlt)[0]
32}
33
34// sizedLanding is sizedRig with animations forced on or off; the scene it
35// returns and every keyLanded and mapLanded of the rig are settled.
36func sizedLanding(t *testing.T, animated bool, overflow string, edit func(*ports.Config)) (*landRig, ports.Scene) {
37	t.Helper()
38	r := startLanding(t, animated, sizedConfig(overflow, edit), ultrawide)
39	return r, r.keyLanded(t, "w", ports.ModAlt)[0]
40}
41
42var frame = ports.Rect{X: 760, Y: 180, W: 1920, H: 1080}
43
44func within(r, o ports.Rect) bool {
45	return r.X >= o.X && r.Y >= o.Y && r.X+r.W <= o.X+o.W && r.Y+r.H <= o.Y+o.H
46}
47
48func TestWorkspaceSizeInheritsByDefault(t *testing.T) {
49	both(t, func(t *testing.T, animated bool) {
50		r := startLanding(t, animated, func(c *ports.Config) { c.Layout.Gaps = 0 }, ultrawide)
51		set := r.mapLanded(t, 1)
52		if set[0].WorkspaceClip != (ports.Rect{}) || set[0].Windows[0].Rect != (ports.Rect{W: 3440, H: 1440}) {
53			t.Fatalf("%+v", set[0])
54		}
55	})
56}
57
58func TestWorkspaceSizeScrollColumnsStayInFrame(t *testing.T) {
59	both(t, func(t *testing.T, animated bool) {
60		r, _ := sizedLanding(t, animated, "scroll", nil)
61		workspaceSizeScrollColumnsStayInFrame(t, r)
62	})
63}
64
65func workspaceSizeScrollColumnsStayInFrame(t *testing.T, r *landRig) {
66	r.mapLanded(t, 1)
67	r.mapLanded(t, 2)
68	s := r.mapLanded(t, 3)[0]
69	if s.WorkspaceClip != frame || s.OutputWidth != 3440 || s.OutputHeight != 1440 {
70		t.Fatalf("clip %+v output %dx%d", s.WorkspaceClip, s.OutputWidth, s.OutputHeight)
71	}
72	for _, w := range s.Windows {
73		if w.Hidden || !w.Rect.Overlaps(frame) {
74			continue
75		}
76		if w.Rect.Y != frame.Y || w.Rect.H != frame.H {
77			t.Fatalf("%+v not framed", w)
78		}
79	}
80	// Focused column (last) ends at the frame's right edge, half the frame wide.
81	last := s.Windows[2].Rect
82	if last != (ports.Rect{X: 1720, Y: 180, W: 960, H: 1080}) {
83		t.Fatal(last)
84	}
85}
86
87func TestWorkspaceSizeFixedColumnsFillFrame(t *testing.T) {
88	both(t, func(t *testing.T, animated bool) {
89		r, _ := sizedLanding(t, animated, "fixed", nil)
90		workspaceSizeFixedColumnsFillFrame(t, r)
91	})
92}
93
94func workspaceSizeFixedColumnsFillFrame(t *testing.T, r *landRig) {
95	r.mapLanded(t, 1)
96	s := r.mapLanded(t, 2)[0]
97	for i, want := range []ports.Rect{{X: 760, Y: 180, W: 960, H: 1080}, {X: 1720, Y: 180, W: 960, H: 1080}} {
98		if s.Windows[i].Rect != want {
99			t.Fatalf("%d: %+v", i, s.Windows[i])
100		}
101	}
102}
103
104func TestWorkspaceSizeFullscreenFillsFrame(t *testing.T) {
105	both(t, func(t *testing.T, animated bool) {
106		r, _ := sizedLanding(t, animated, "scroll", nil)
107		r.mapLanded(t, 1)
108		r.mapLanded(t, 2)
109		s := r.keyLanded(t, "f", ports.ModAlt|ports.ModShift)[0]
110		full := 0
111		for _, w := range s.Windows {
112			if w.Fullscreen {
113				full++
114				if w.Rect != frame {
115					t.Fatalf("%+v", w)
116				}
117			}
118		}
119		if full != 1 {
120			t.Fatal(s.Windows)
121		}
122	})
123}
124
125func TestWorkspaceSizeFixedFullscreenKeepsFrame(t *testing.T) {
126	both(t, func(t *testing.T, animated bool) {
127		r, _ := sizedLanding(t, animated, "fixed", nil)
128		r.mapLanded(t, 1)
129		r.mapLanded(t, 2)
130		s := r.keyLanded(t, "f", ports.ModAlt|ports.ModShift)[0]
131		if s.WorkspaceClip != frame {
132			t.Fatalf("fullscreen clip %+v", s.WorkspaceClip)
133		}
134		for _, w := range s.Windows {
135			if w.Fullscreen && w.Rect != frame {
136				t.Fatalf("%+v", w)
137			}
138		}
139		// Reload keeps the size; removing it frees the workspace.
140		cfg := r.cfg
141		r.reload <- ports.ConfigChanged{Config: cfg}
142		if s := r.land(receive(t, r.scenes))[0]; s.WorkspaceClip != frame {
143			t.Fatalf("after reload %+v", s.WorkspaceClip)
144		}
145		cfg.Workspaces = []ports.WorkspaceConfig{{Name: "focus", LayoutRules: ports.LayoutRules{Overflow: "fixed"}}}
146		r.reload <- ports.ConfigChanged{Config: cfg}
147		if s := r.land(receive(t, r.scenes))[0]; s.WorkspaceClip != (ports.Rect{}) {
148			t.Fatalf("after size removal %+v", s.WorkspaceClip)
149		}
150	})
151}
152
153func TestWorkspaceSizeFloatsContained(t *testing.T) {
154	r, _ := sizedRig(t, "scroll", nil)
155	r.mapWindow(t, 1)
156	r.client <- ports.WindowMapped{ID: 2, Floating: true, Width: 5000, Height: 4000}
157	s := receive(t, r.scenes)[0]
158	for _, w := range s.Windows {
159		if w.Floating && !within(w.Rect, frame) {
160			t.Fatalf("float %+v outside %+v", w.Rect, frame)
161		}
162	}
163}
164
165func TestWorkspaceSizeReloadAndRemoval(t *testing.T) {
166	both(t, func(t *testing.T, animated bool) {
167		r, _ := sizedLanding(t, animated, "scroll", nil)
168		workspaceSizeReloadAndRemoval(t, r)
169	})
170}
171
172func workspaceSizeReloadAndRemoval(t *testing.T, r *landRig) {
173	r.mapLanded(t, 1)
174	cfg := r.cfg
175	cfg.Workspaces = []ports.WorkspaceConfig{{Name: "focus", Size: [2]int{1000, 600}}}
176	r.reload <- ports.ConfigChanged{Config: cfg}
177	s := receive(t, r.scenes)[0]
178	if want := (ports.Rect{X: 1220, Y: 420, W: 1000, H: 600}); s.WorkspaceClip != want || s.Windows[0].Rect != want {
179		t.Fatalf("%+v %+v", s.WorkspaceClip, s.Windows[0])
180	}
181	// Larger than the monitor clamps to it: no clip, no invisible area.
182	cfg.Workspaces = []ports.WorkspaceConfig{{Name: "focus", Size: [2]int{9000, 9000}}}
183	r.reload <- ports.ConfigChanged{Config: cfg}
184	s = receive(t, r.scenes)[0]
185	if s.WorkspaceClip != (ports.Rect{}) || s.Windows[0].Rect != (ports.Rect{W: 3440, H: 1440}) {
186		t.Fatalf("%+v %+v", s.WorkspaceClip, s.Windows[0])
187	}
188	cfg.Workspaces = []ports.WorkspaceConfig{{Name: "focus", Size: [2]int{1920, 1080}}}
189	r.reload <- ports.ConfigChanged{Config: cfg}
190	if s = receive(t, r.scenes)[0]; s.WorkspaceClip != frame {
191		t.Fatal(s.WorkspaceClip)
192	}
193	cfg.Workspaces = []ports.WorkspaceConfig{{Name: "focus"}}
194	r.reload <- ports.ConfigChanged{Config: cfg}
195	if s = receive(t, r.scenes)[0]; s.WorkspaceClip != (ports.Rect{}) || s.Windows[0].Rect != (ports.Rect{W: 3440, H: 1440}) {
196		t.Fatalf("%+v %+v", s.WorkspaceClip, s.Windows[0])
197	}
198}
199
200func TestWorkspaceSizeFollowsOutputResize(t *testing.T) {
201	both(t, func(t *testing.T, animated bool) {
202		r, _ := sizedLanding(t, animated, "scroll", nil)
203		workspaceSizeFollowsOutputResize(t, r)
204	})
205}
206
207func workspaceSizeFollowsOutputResize(t *testing.T, r *landRig) {
208	r.mapLanded(t, 1)
209	smaller := ultrawide
210	smaller.Width, smaller.Height = 1600, 900
211	s := r.land(r.plug(t, smaller))[0]
212	// 1920x1080 no longer fits: it clamps to the monitor.
213	if s.WorkspaceClip != (ports.Rect{}) || s.Windows[0].Rect != (ports.Rect{W: 1600, H: 900}) {
214		t.Fatalf("%+v %+v", s.WorkspaceClip, s.Windows[0])
215	}
216	s = r.land(r.plug(t, ultrawide))[0]
217	if s.WorkspaceClip != frame || s.Windows[0].Rect != frame {
218		t.Fatalf("%+v %+v", s.WorkspaceClip, s.Windows[0])
219	}
220}
221
222func TestWorkspaceSizeReservedZoneIntersectsFrame(t *testing.T) {
223	both(t, func(t *testing.T, animated bool) {
224		r, _ := sizedLanding(t, animated, "scroll", nil)
225		workspaceSizeReservedZoneIntersectsFrame(t, r)
226	})
227}
228
229func workspaceSizeReservedZoneIntersectsFrame(t *testing.T, r *landRig) {
230	// A bar inside the top of the monitor but above the frame reserves nothing of it.
231	bar := ports.LayerSurface{ID: 9, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 100, ExclusiveZone: 100}
232	r.client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
233	receive(t, r.scenes)
234	s := r.mapLanded(t, 1)[0]
235	if s.Windows[0].Rect != frame {
236		t.Fatalf("%+v", s.Windows[0])
237	}
238	// A bar reaching into the frame shrinks it from the top.
239	bar.Height, bar.ExclusiveZone = 300, 300
240	r.client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
241	s = receive(t, r.scenes)[0]
242	if want := (ports.Rect{X: 760, Y: 300, W: 1920, H: 960}); s.Windows[0].Rect != want || s.WorkspaceClip != frame {
243		t.Fatalf("%+v clip %+v", s.Windows[0], s.WorkspaceClip)
244	}
245}
246
247func TestWorkspaceSizeFractionalScaleUnchanged(t *testing.T) {
248	r, _ := sizedRig(t, "scroll", func(c *ports.Config) {
249		c.Outputs = []ports.OutputConfig{{Name: "DP-3", Scale: 2}}
250	})
251	s := r.mapWindow(t, 1)[0]
252	// The monitor is 1720x720 logical; a 1920x1080 workspace clamps to it.
253	if s.OutputWidth != 1720 || s.OutputHeight != 720 || s.Scale != 2 || s.WorkspaceClip != (ports.Rect{}) {
254		t.Fatalf("%dx%d scale %v clip %+v", s.OutputWidth, s.OutputHeight, s.Scale, s.WorkspaceClip)
255	}
256}
257
258func TestWorkspaceSizeInputOutsideFrame(t *testing.T) {
259	r, _ := sizedRig(t, "scroll", nil)
260	r.mapWindow(t, 1)
261	r.mapWindow(t, 2)
262	for len(r.commands) > 0 {
263		<-r.commands
264	}
265	// Inside the frame the window under the pointer gets the focus; window 1
266	// is scrolled partly off but its visible part outside the frame is dead.
267	r.input <- ports.PointerMotion{X: 2000, Y: 700}
268	if v, ok := command(t, r.commands).(ports.PointerFocus); !ok || v.ID != 2 {
269		t.Fatal(v)
270	}
271	r.input <- ports.PointerMotion{X: 100, Y: 700}
272	for {
273		if v, ok := command(t, r.commands).(ports.PointerFocus); ok {
274			if v.ID != 0 {
275				t.Fatalf("outside the frame: %+v", v)
276			}
277			break
278		}
279	}
280}

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