1package core
2
3import (
4 "math"
5 "slices"
6 "testing"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11func maximizedOverview() *Monitor {
12 m := newMonitor("", "")
13 m.SetOutput(600, 400)
14 m.SetOverflow(OverflowFixed)
15 m.SetMaxColumns(3)
16 for id := WindowID(1); id <= 4; id++ {
17 m.AddWindow(id)
18 }
19 m.Current().FocusID(2)
20 m.Current().ToggleFullWidth()
21 return m
22}
23
24func TestOverviewMaximizedGeometryAndNavigation(t *testing.T) {
25 m := maximizedOverview()
26 w := m.Current()
27 w.Columns[2].Windows = append(w.Columns[2].Windows, 5)
28 rects := w.hiddenColumnRects()
29 m.ToggleOverview()
30 if got := w.stack(); !slices.Equal(got, []stackItem{{stackColumn, 2}, {kind: stackColumns}}) {
31 t.Fatalf("items %v", got)
32 }
33 if p := previewOf(t, m.Layout(), 2); p.Hidden || p.Peek || !p.Focused {
34 t.Fatalf("maximized front %+v", p)
35 }
36 ps := m.Layout()
37 for _, id := range []WindowID{1, 3, 4, 5} {
38 p := previewOf(t, ps, id)
39 i := w.columnOf(id)
40 r := rects[i]
41 if id == 3 || id == 5 {
42 r = stackRects(r, 2, w.gap())[map[WindowID]int{3: 0, 5: 1}[id]]
43 }
44 if p.Hidden || !p.Peek || p.Focused || math.Abs(float64(p.Rect.W)-float64(r.W)*p.Preview) > 1 || math.Abs(float64(p.Rect.H)-float64(r.H)*p.Preview) > 1 {
45 t.Fatalf("hidden tile %d: %+v cell %+v", id, p, r)
46 }
47 for _, other := range []WindowID{1, 3, 4, 5} {
48 if id < other && p.Rect.Overlaps(previewOf(t, ps, other).Rect) {
49 t.Fatalf("hidden tiles %d/%d overlap", id, other)
50 }
51 }
52 }
53 // The hidden columns fill the card as if the maximized column were
54 // gone: no hole where it was.
55 area := 0
56 for _, id := range []WindowID{1, 3, 4, 5} {
57 r := previewOf(t, ps, id).Rect
58 area += r.W * r.H
59 }
60 z := previewOf(t, ps, 1).Preview
61 full := float64(w.Usable.W) * float64(w.Usable.H) * z * z
62 if float64(area) < 0.8*full {
63 t.Fatalf("hidden tiles cover %d of %.0f: hole left by the maximized column", area, full)
64 }
65 m.OverviewMove(0, -1)
66 if m.stackFront(w).kind != stackColumns || m.hiddenColumn(w) != 0 || !previewOf(t, m.Layout(), 1).Focused || w.Focus != 1 || !w.Columns[1].FullWidth {
67 t.Fatal("up changed real maximization or failed to select hidden card")
68 }
69 m.OverviewMove(1, 0)
70 if m.hiddenColumn(w) != 2 || !previewOf(t, m.Layout(), 3).Focused || w.Focus != 1 {
71 t.Fatal("right did not select column 3 provisionally")
72 }
73 m.OverviewMove(-1, 0)
74 if m.hiddenColumn(w) != 0 {
75 t.Fatal("left did not skip maximized slot")
76 }
77 m.OverviewMove(1, 0)
78 m.ToggleOverview()
79 if id, _ := w.Focused(); id != 3 || !w.Columns[2].FullWidth || w.Columns[1].FullWidth || !previewOf(t, w.Layout(), 1).Hidden {
80 t.Fatalf("confirm focus %d columns %+v", id, w.Columns)
81 }
82 m.ToggleOverview()
83 if m.hiddenColumn(w) != 1 || m.stackFront(w).kind != stackColumn {
84 t.Fatal("previously maximized column not selected behind front")
85 }
86 m.OverviewMove(0, -1)
87 if p := previewOf(t, m.Layout(), 2); !p.Focused || p.Peek {
88 t.Fatalf("MRU selection %+v", p)
89 }
90 m.CancelOverview()
91}
92
93func TestOverviewMaximizedCancelAndClick(t *testing.T) {
94 m := maximizedOverview()
95 w := m.Current()
96 w.AddFloating(9, 600, 400)
97 w.FocusID(2)
98 before := slices.Clone(order(w))
99 m.ToggleOverview()
100 m.OverviewMove(0, -1)
101 m.OverviewMove(0, -1)
102 m.CancelOverview()
103 if id, _ := w.Focused(); id != 2 || !w.Columns[1].FullWidth || !slices.Equal(order(w), before) {
104 t.Fatalf("cancel focus %d order %v", id, order(w))
105 }
106 m.ToggleOverview()
107 p := previewOf(t, m.Layout(), 3)
108 if !p.Peek {
109 t.Fatalf("peek %+v", p)
110 }
111 m.OverviewPick(3)
112 if id, _ := w.Focused(); id != 3 || !w.Columns[2].FullWidth {
113 t.Fatalf("click focus %d max %+v", id, w.Columns)
114 }
115}
116
117func TestOverviewMaximizedFloatWrapAndNeighbor(t *testing.T) {
118 m := maximizedOverview()
119 w := m.Current()
120 w.AddFloating(9, 600, 400)
121 m.Focus(1)
122 m.AddWindow(10)
123 m.AddWindow(11)
124 m.Current().FocusID(10)
125 m.Current().ToggleFullWidth()
126 m.Focus(0)
127 m.ToggleOverview()
128 if got := w.stack(); !slices.Equal(got, []stackItem{{stackFloat, 9}, {stackColumn, 2}, {kind: stackColumns}}) {
129 t.Fatalf("stack %v", got)
130 }
131 if p, q := previewOf(t, m.Layout(), 10), previewOf(t, m.Layout(), 11); !p.Peek || !q.Peek || p.Focused || q.Focused {
132 t.Fatalf("neighbor %+v %+v", p, q)
133 }
134 m.OverviewMove(0, -1)
135 if m.stackFront(w) != (stackItem{stackColumn, 2}) {
136 t.Fatalf("front %v", m.stackFront(w))
137 }
138 m.OverviewMove(0, 1)
139 if m.stackFront(w) != (stackItem{stackFloat, 9}) {
140 t.Fatalf("back %v", m.stackFront(w))
141 }
142 m.CancelOverview()
143}
144
145func TestOverviewMaximizedLeftAndStash(t *testing.T) {
146 m := maximizedOverview()
147 w := m.Current()
148 w.FocusID(4)
149 w.ToggleWindowStash()
150 w.FocusID(2)
151 w.ToggleFullWidth()
152 m.ToggleOverview()
153 m.OverviewMove(-1, 0)
154 if m.cardAt(w) >= 0 || m.stackFront(w).kind != stackColumns {
155 t.Fatalf("left should send card behind: %v", m.stackFront(w))
156 }
157 m.OverviewMove(0, 1)
158 if m.stackFront(w) != (stackItem{stackColumn, 2}) {
159 t.Fatalf("front %v", m.stackFront(w))
160 }
161 m.CancelOverview()
162}
163
164// Escape restores the opening column state after a real workspace mutation.
165func TestOverviewCancelRestoresFullWidthAfterMutation(t *testing.T) {
166 m := maximizedOverview()
167 w := m.Current()
168 m.ToggleOverview()
169 w.ToggleFullWidth()
170 if w.Columns[1].FullWidth {
171 t.Fatal("workspace mutation did not toggle maximization")
172 }
173 m.CancelOverview()
174 if w.Focus != 1 || !w.Columns[1].FullWidth {
175 t.Fatalf("cancel lost maximization: focus %d columns %+v", w.Focus, w.Columns)
176 }
177 // The snapshot follows a surviving column if another column disappears.
178 m.ToggleOverview()
179 w.RemoveWindow(1)
180 w.ToggleFullWidth()
181 m.CancelOverview()
182 if id, _ := w.Focused(); id != 2 || !w.Columns[0].FullWidth {
183 t.Fatalf("cancel after removal: focus %d columns %+v", id, w.Columns)
184 }
185}
186
187// Escape maximizes only the column holding the window focused at opening,
188// where the focus returns, even when binds split its column meanwhile.
189func TestOverviewCancelFullWidthAfterSplit(t *testing.T) {
190 m := maximizedOverview()
191 w := m.Current()
192 w.FocusID(3)
193 w.ConsumeOrExpel(-1) // column 2 holds [2 3]
194 w.FocusID(3)
195 w.ToggleFullWidth()
196 m.ToggleOverview()
197 w.FocusID(3)
198 w.ConsumeOrExpel(1) // 3 leaves for another column
199 w.FocusID(2)
200 w.ToggleFullWidth() // 2's column maximized instead
201 m.CancelOverview()
202 maxed := 0
203 for _, c := range w.Columns {
204 if c.FullWidth {
205 maxed++
206 if !slices.Contains(c.Windows, 3) {
207 t.Fatalf("wrong column maximized: %+v", w.Columns)
208 }
209 }
210 }
211 if id, _ := w.Focused(); maxed != 1 || id != 3 {
212 t.Fatalf("%d maximized, focus %d: %+v", maxed, id, w.Columns)
213 }
214}
215
216// In scroll overflow, Escape leaves a maximized column the focus left.
217func TestOverviewCancelKeepsScrollFullWidth(t *testing.T) {
218 m := maximizedOverview()
219 w := m.Current()
220 w.Overflow = OverflowScroll
221 w.FocusID(3) // column 2 stays maximized in scroll overflow
222 if !w.Columns[1].FullWidth {
223 t.Fatal("setup: column 2 not maximized")
224 }
225 m.ToggleOverview()
226 m.CancelOverview()
227 if !w.Columns[1].FullWidth {
228 t.Fatalf("escape cleared column 2: %+v", w.Columns)
229 }
230}
231
232// A selected single-column card follows its anchor when earlier columns go.
233func TestOverviewHiddenSelectionFollowsWindowAfterRemoval(t *testing.T) {
234 m := maximizedOverview()
235 w := m.Current()
236 m.ToggleOverview()
237 m.OverviewMove(0, -1)
238 m.OverviewMove(1, 0) // select column 3 inside the group
239 w.RemoveWindow(1)
240 if p := previewOf(t, m.Layout(), 3); !p.Focused || p.Peek {
241 t.Fatalf("selection moved: %+v", p)
242 }
243 m.ToggleOverview()
244 if id, _ := w.Focused(); id != 3 || !w.Columns[1].FullWidth {
245 t.Fatalf("confirm %d %+v", id, w.Columns)
246 }
247}
248
249func TestOverviewHiddenSelectionRemovedFallsBack(t *testing.T) {
250 m := maximizedOverview()
251 w := m.Current()
252 m.ToggleOverview()
253 m.OverviewMove(0, -1)
254 m.OverviewMove(1, 0) // select column 3 inside the group
255 w.RemoveWindow(3)
256 if p := previewOf(t, m.Layout(), 4); !p.Focused {
257 t.Fatalf("fallback %+v", p)
258 }
259 m.ToggleOverview()
260 if id, _ := w.Focused(); id != 4 {
261 t.Fatalf("fallback %d", id)
262 }
263}
264
265func TestOverviewMaximizedPreviewsKeepConfigures(t *testing.T) {
266 m := maximizedOverview()
267 w := m.Current()
268 s := newConfigures()
269 before := map[WindowID]ports.ConfigureWindow{}
270 for _, p := range w.Layout() {
271 // Hidden clients retain their last unmaximized configure.
272 if p.Hidden {
273 r := w.columnRectsFor(true)[int(p.ID)-1]
274 p.Rect, p.Hidden = r, false
275 }
276 v, _ := s.next(p, configureTarget{output: "test", client: p.Rect, view: whole(w.Usable)})
277 s.mark(v)
278 before[p.ID] = v
279 }
280 m.ToggleOverview()
281 m.OverviewMove(0, -1)
282 for _, p := range m.Layout() {
283 if p.ID < 1 || p.ID > 4 {
284 continue
285 }
286 v, _ := s.next(p, configureTarget{output: "test", view: whole(w.Usable)})
287 if v.Width != before[p.ID].Width || v.Height != before[p.ID].Height {
288 t.Fatalf("%d resized from %+v to %+v", p.ID, before[p.ID], v)
289 }
290 }
291}
292
293func TestOverviewScrollFullWidthUnchanged(t *testing.T) {
294 m := maximizedOverview()
295 w := m.Current()
296 w.Overflow = OverflowScroll
297 m.ToggleOverview()
298 if got := w.stack(); !slices.Equal(got, []stackItem{{kind: stackColumns}}) {
299 t.Fatalf("scroll items %v", got)
300 }
301 for _, id := range []WindowID{1, 3, 4} {
302 if p := previewOf(t, m.Layout(), id); p.Hidden || p.Peek {
303 t.Fatalf("scroll column %d %+v", id, p)
304 }
305 }
306}