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}