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