1package core
2
3import (
4 "context"
5 "slices"
6 "testing"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11func TestToggleWindowStash(t *testing.T) {
12 for _, stacked := range []bool{false, true} {
13 t.Run(map[bool]string{false: "column", true: "stack"}[stacked], func(t *testing.T) {
14 w := workspace()
15 w.border = 2
16 w.AddWindow(1)
17 w.AddWindow(2)
18 w.Columns[1].Width = Width{Pixels: 35}
19 if stacked {
20 w.Columns[1].Windows = []WindowID{3, 2}
21 w.Columns[1].Focus = 1
22 }
23 w.Apply(ActionToggleWindowStash)
24 if len(w.Stash) != 1 || len(w.Floats) != 0 || !w.Stash[0].back.holdsStack(Column{Windows: []WindowID{3}}) && stacked {
25 t.Fatalf("stash %+v", w.Stash)
26 }
27 p := placement(w, 2)
28 if !p.Focused || !p.Floating || p.Rect != (Rect{X: 10, Y: 8, W: 80, H: 64}) {
29 t.Fatalf("placement %+v", p)
30 }
31 w.ResizeFloating(2, 12, 14)
32 if placement(w, 2).Rect != p.Rect {
33 t.Fatal("client resize moved stashed window")
34 }
35 w.Apply(ActionToggleWindowStash)
36 id, _ := w.Focused()
37 if id != 2 || len(w.Stash) != 0 || w.Focus != 1 || w.Columns[1].Width != (Width{Pixels: 35}) || !slices.Contains(w.Columns[1].Windows, 2) {
38 t.Fatalf("round trip: %+v", w)
39 }
40 })
41 }
42 w := workspace()
43 w.AddFloating(4, 20, 10)
44 w.Apply(ActionToggleWindowStash)
45 if len(w.Floats) != 0 || len(w.Columns) != 1 || w.Columns[0].Windows[0] != 4 {
46 t.Fatalf("native float: %+v", w)
47 }
48}
49
50// placement returns the placement of id in the workspace layout.
51func placement(w *Workspace, id WindowID) Placement {
52 for _, p := range w.Layout() {
53 if p.ID == id {
54 return p
55 }
56 }
57 return Placement{}
58}
59
60// The stash is a strip: new windows go last, selected and centred; the
61// neighbors peek in beside it, stash.gap away; focus moves inside it and stops at its
62// ends; up and down do nothing there.
63func TestStashStrip(t *testing.T) {
64 w := workspace()
65 w.stashGap = 5
66 for id := WindowID(1); id <= 4; id++ {
67 w.AddWindow(id)
68 }
69 for _, id := range []WindowID{4, 3, 2} {
70 w.FocusID(id)
71 w.Apply(ActionToggleWindowStash)
72 }
73 if got := []WindowID{w.Stash[0].ID, w.Stash[1].ID, w.Stash[2].ID}; !slices.Equal(got, []WindowID{4, 3, 2}) {
74 t.Fatalf("order %v", got)
75 }
76 if id, _ := w.Focused(); id != 2 || w.stashAt != 2 {
77 t.Fatalf("focused %d at %d", id, w.stashAt)
78 }
79 center := Rect{X: 10, Y: 8, W: 80, H: 64}
80 if p := placement(w, 2); p.Rect != center || p.Peek || p.Hidden {
81 t.Fatalf("selected %+v", p)
82 }
83 if p := placement(w, 3); !p.Peek || p.Rect != (Rect{X: 5 - 80, Y: 8, W: 80, H: 64}) {
84 t.Fatalf("left peek %+v", p)
85 }
86 if p := placement(w, 4); !p.Hidden {
87 t.Fatalf("far window %+v", p)
88 }
89 w.Apply(ActionFocusColumnRight)
90 if id, _ := w.Focused(); id != 2 {
91 t.Fatalf("moved past the end: %d", id)
92 }
93 w.Apply(ActionFocusColumnLeft)
94 if p := placement(w, 2); !p.Peek || p.Rect != (Rect{X: 95, Y: 8, W: 80, H: 64}) {
95 t.Fatalf("right peek %+v", p)
96 }
97 w.Apply(ActionFocusColumnLeft)
98 w.Apply(ActionFocusColumnLeft)
99 w.Apply(ActionFocusWindowUp)
100 if id, _ := w.Focused(); id != 4 {
101 t.Fatalf("focused %d, want the first", id)
102 }
103 // Column actions do nothing while the stash has the focus.
104 w.Apply(ActionMoveColumnLeft)
105 w.Apply(ActionCycleColumnWidth)
106 if w.Columns[0].Windows[0] != 1 || w.Columns[0].Width != (Width{}) {
107 t.Fatalf("column changed: %+v", w.Columns)
108 }
109 // A gap as wide as the margin leaves nothing to show: neighbors wait
110 // off screen.
111 w.stashGap = 10
112 if p := placement(w, 3); !p.Hidden || p.Peek {
113 t.Fatalf("no peek: %+v", p)
114 }
115}
116
117// Hiding the stash is the way out: tiles get the focus, a new stashed
118// window shows it again, and the native floats stay as they are.
119func TestToggleStashVisible(t *testing.T) {
120 w := workspace()
121 w.Apply(ActionToggleStashVisible)
122 w.AddWindow(1)
123 w.AddWindow(2)
124 w.AddWindow(3)
125 w.Apply(ActionToggleWindowStash)
126 w.FocusID(2)
127 w.Apply(ActionToggleWindowStash)
128 w.Apply(ActionFocusColumnLeft)
129 w.Apply(ActionToggleStashVisible)
130 if id, _ := w.Focused(); id != 1 || !w.stashHidden {
131 t.Fatalf("focused %d hidden %v", id, w.stashHidden)
132 }
133 for _, id := range []WindowID{2, 3} {
134 if p := placement(w, id); !p.Hidden || p.Focused {
135 t.Fatalf("hidden %+v", p)
136 }
137 }
138 if w.FocusID(2) {
139 t.Fatal("focused hidden stash window")
140 }
141 // Shown from a tile: the stash takes the focus on its selected window.
142 w.Apply(ActionToggleStashVisible)
143 if id, _ := w.Focused(); id != 3 || w.stashHidden {
144 t.Fatalf("restored focus %d", id)
145 }
146 w.Apply(ActionToggleStashVisible)
147 w.FocusID(1)
148 w.Apply(ActionToggleWindowStash)
149 if id, _ := w.Focused(); id != 1 || w.stashHidden || w.stashAt != 2 {
150 t.Fatalf("new stashed window: focused %d hidden %v", id, w.stashHidden)
151 }
152 // A native dialog stays when the stash hides.
153 w.AddFloating(9, 10, 10)
154 w.Apply(ActionToggleStashVisible)
155 if p := placement(w, 9); p.Hidden || !w.stashHidden {
156 t.Fatalf("dialog hidden with the stash: %+v", p)
157 }
158 if id, _ := w.Focused(); id != 9 {
159 t.Fatalf("focused %d, want the dialog", id)
160 }
161}
162
163// Closing the selected stashed window selects its right neighbor, else its
164// left one, else gives the focus back to the tiles.
165func TestStashClose(t *testing.T) {
166 w := workspace()
167 for id := WindowID(1); id <= 4; id++ {
168 w.AddWindow(id)
169 }
170 for _, id := range []WindowID{4, 3, 2} {
171 w.FocusID(id)
172 w.Apply(ActionToggleWindowStash)
173 }
174 w.FocusID(3) // the middle one
175 w.RemoveWindow(3)
176 if id, _ := w.Focused(); id != 2 {
177 t.Fatalf("after middle: %d", id)
178 }
179 w.RemoveWindow(2)
180 if id, _ := w.Focused(); id != 4 {
181 t.Fatalf("after last: %d", id)
182 }
183 w.RemoveWindow(4)
184 if id, _ := w.Focused(); id != 1 || len(w.Stash) != 0 || w.stashFocus {
185 t.Fatalf("after all: %d", id)
186 }
187}
188
189// A fullscreen window is exclusive: a new window waits hidden without
190// moving the view, in scroll and fixed overflow, and shows once it leaves.
191func TestExclusiveFullscreenNewWindow(t *testing.T) {
192 for _, overflow := range []Overflow{OverflowScroll, OverflowFixed} {
193 w := workspace()
194 w.Overflow = overflow
195 w.AddWindow(1)
196 w.SetFullscreen(1, true)
197 w.AddWindow(2)
198 for _, p := range w.Layout() {
199 if p.ID == 1 && (!p.Fullscreen || !p.Focused || p.Hidden) || p.ID == 2 && !p.Hidden {
200 t.Fatalf("overflow %v: %+v", overflow, p)
201 }
202 }
203 if w.ToggleStashVisible(); w.stashHidden {
204 t.Fatal("floats toggled under fullscreen")
205 }
206 w.SetFullscreen(1, false)
207 for _, p := range w.Layout() {
208 if p.ID == 2 && p.Hidden {
209 t.Fatalf("overflow %v: new window still hidden", overflow)
210 }
211 }
212 }
213}
214
215// A window mapped under a covering fullscreen float with no column waits
216// hidden as the first column.
217func TestFullscreenFloatNewWindowNoColumns(t *testing.T) {
218 w := workspace()
219 w.AddFloating(1, 300, 200)
220 w.SetFullscreen(1, true)
221 w.AddWindow(2)
222 if len(w.Columns) != 1 || w.Columns[0].Windows[0] != 2 {
223 t.Fatalf("columns %+v", w.Columns)
224 }
225 if id, _ := w.Focused(); id != 1 {
226 t.Fatalf("focus %d", id)
227 }
228}
229
230// Under a covering fullscreen float, or any fixed-overflow fullscreen,
231// binds act on the window on screen, never on a column it hides.
232func TestFullscreenPinsFocus(t *testing.T) {
233 w := workspace()
234 w.AddWindow(1)
235 w.AddWindow(2)
236 w.AddFloating(3, 20, 10)
237 w.SetFullscreen(3, true)
238 w.focusCover()
239 w.Apply(ActionMoveColumnLeft)
240 w.Apply(ActionCycleColumnWidth)
241 if id, _ := w.Focused(); id != 3 || w.fullscreen != 3 || w.Columns[0].Windows[0] != 1 || w.Columns[1].Width != (Width{}) {
242 t.Fatalf("focused %d fullscreen %d columns %+v", id, w.fullscreen, w.Columns)
243 }
244 if e := w.Apply(ActionCloseWindow); e.Close != 3 {
245 t.Fatalf("close %d, want the fullscreen window", e.Close)
246 }
247}
248
249// A focus move from a covering fullscreen window leaves fullscreen for the
250// window on that side; with none there, fullscreen stays.
251func TestFocusMoveLeavesFullscreen(t *testing.T) {
252 w := workspace()
253 w.AddWindow(1)
254 w.AddWindow(2)
255 w.AddFloating(3, 20, 10)
256 w.SetFullscreen(3, true)
257 w.FocusColumn(-1)
258 if id, _ := w.Focused(); id == 3 || w.fullscreen != 0 {
259 t.Fatalf("float: focused %d fullscreen %d", id, w.fullscreen)
260 }
261 f := workspace()
262 f.Overflow = OverflowFixed
263 f.AddWindow(1)
264 f.AddWindow(2)
265 f.SetFullscreen(2, true)
266 if f.FocusColumn(1); f.fullscreen != 2 || f.Focus != 1 {
267 t.Fatalf("fixed edge: focus %d fullscreen %d", f.Focus, f.fullscreen)
268 }
269 if f.FocusColumn(-1); f.fullscreen != 0 || f.Focus != 0 {
270 t.Fatalf("fixed: focus %d fullscreen %d", f.Focus, f.fullscreen)
271 }
272 // Side by side: nothing above, so up keeps fullscreen and reports
273 // the edge (the monitor goes to the workspace above).
274 f.Focus = 1
275 f.SetFullscreen(2, true)
276 if f.FocusWindow(-1) || f.fullscreen != 2 || f.Focus != 1 {
277 t.Fatalf("fixed up: focus %d fullscreen %d", f.Focus, f.fullscreen)
278 }
279 // Stacked: up reaches the window above in the column.
280 f.SetFullscreen(2, false)
281 f.stackWindow(2, 1)
282 f.FocusID(2)
283 f.SetFullscreen(2, true)
284 if !f.FocusWindow(-1) || f.fullscreen != 0 {
285 t.Fatalf("stacked up: fullscreen %d", f.fullscreen)
286 }
287 if id, _ := f.Focused(); id != 1 {
288 t.Fatalf("stacked up: focused %d", id)
289 }
290 // Scroll overflow: moving off a fullscreen column still works; it no
291 // longer covers.
292 s := workspace()
293 s.AddWindow(1)
294 s.AddWindow(2)
295 s.SetFullscreen(2, true)
296 if s.FocusColumn(-1); s.Focus != 0 || s.cover() != 0 {
297 t.Fatalf("scroll: focus %d cover %d", s.Focus, s.cover())
298 }
299}
300
301// A fullscreen window does not float until it leaves fullscreen.
302func TestToggleWindowStashIgnoresFullscreen(t *testing.T) {
303 w := workspace()
304 w.AddWindow(1)
305 w.SetFullscreen(1, true)
306 w.Apply(ActionToggleWindowStash)
307 if len(w.Stash) != 0 || w.fullscreen != 1 {
308 t.Fatalf("fullscreen window floated: %+v", w)
309 }
310 w.SetFullscreen(1, false)
311 w.Apply(ActionToggleWindowStash)
312 if len(w.Stash) != 1 {
313 t.Fatal("window did not float after leaving fullscreen")
314 }
315}
316
317// A lone maximized column comes back maximized.
318func TestToggleWindowStashKeepsFullWidth(t *testing.T) {
319 w := workspace()
320 w.Overflow = OverflowFixed
321 w.AddWindow(1)
322 w.AddWindow(2)
323 w.Apply(ActionMaximizeColumn)
324 w.Apply(ActionToggleWindowStash)
325 w.Apply(ActionToggleWindowStash)
326 if id, _ := w.Focused(); id != 2 || !w.Columns[w.Focus].FullWidth {
327 t.Fatalf("focused %d, column %+v", id, w.Columns[w.Focus])
328 }
329}
330
331// A lone expanded column (fixed overflow) comes back expanded.
332func TestToggleWindowStashKeepsExpanded(t *testing.T) {
333 w := workspace()
334 w.Overflow = OverflowFixed
335 w.SetMaxColumns(3)
336 w.AddWindow(1)
337 w.AddWindow(2)
338 w.CycleWidth()
339 if !w.Columns[w.Focus].Expanded {
340 t.Fatal("column not expanded")
341 }
342 w.Apply(ActionToggleWindowStash)
343 w.Apply(ActionToggleWindowStash)
344 if id, _ := w.Focused(); id != 2 || !w.Columns[w.Focus].Expanded {
345 t.Fatalf("focused %d, column %+v", id, w.Columns[w.Focus])
346 }
347}
348
349// A client fullscreen request of a hidden stashed window leaves it hidden
350// on its workspace, even in fixed overflow (which moves fullscreen windows
351// out).
352func TestHiddenFloatFullscreenRequest(t *testing.T) {
353 m := newMonitor("", "")
354 m.SetOutput(100, 80)
355 m.SetOverflow(OverflowFixed)
356 m.AddWindow(1)
357 m.AddWindow(2)
358 w := m.Current()
359 w.ToggleWindowStash()
360 w.ToggleStashVisible()
361 m.SetFullscreen(2, true)
362 if m.Current() != w || w.stashIndex(2) < 0 || !w.stashHidden || w.fullscreen != 0 {
363 t.Fatalf("hidden stash left or showed: %+v", w)
364 }
365 w.ToggleStashVisible()
366 if w.fullscreen != 2 {
367 t.Fatalf("fullscreen lost on show: %d", w.fullscreen)
368 }
369}
370
371// A fullscreen stashed window (a game in scanout) keeps the stash shown:
372// hiding it would drop scanout and VRR.
373func TestToggleStashVisibleFullscreen(t *testing.T) {
374 w := workspace()
375 w.AddWindow(1)
376 w.AddWindow(2)
377 w.ToggleWindowStash()
378 w.SetFullscreen(2, true)
379 w.Apply(ActionToggleStashVisible)
380 if id, _ := w.Focused(); id != 2 || w.fullscreen != 2 || w.stashHidden {
381 t.Fatalf("fullscreen stash hidden: focused %d fullscreen %d", id, w.fullscreen)
382 }
383}
384
385func TestFloatingSceneDimAndConfigure(t *testing.T) {
386 cfg := ports.Config{}
387 cfg.Keyboard.CmdKey = "super"
388 cfg.Layout.MaxColumns = 2
389 cfg.Floating.Dim = 0.3
390 cfg.Border.Width = 2
391 commands := make(chan ports.ClientCommand, 128)
392 scenes := make(chan []ports.Scene, 1)
393 c, err := New(cfg, Channels{Commands: commands, Scenes: scenes})
394 if err != nil {
395 t.Fatal(err)
396 }
397 w := c.cur().mon.Current()
398 c.cur().mon.SetOutput(100, 80)
399 w.AddWindow(1)
400 ctx := context.Background()
401 check := func(want float64) {
402 t.Helper()
403 if err := c.publish(ctx); err != nil {
404 t.Fatal(err)
405 }
406 if s := (<-scenes)[0]; s.Dim != want {
407 t.Fatalf("dim %v want %v", s.Dim, want)
408 }
409 }
410 check(0)
411 w.ToggleWindowStash()
412 check(0.3)
413 found := false
414 for len(commands) > 0 {
415 if v, ok := (<-commands).(ports.ConfigureWindow); ok && v.ID == 1 {
416 if v.Floating {
417 found = true
418 if v.Width != 76 || v.Height != 60 {
419 t.Fatalf("imposed configure %+v", v)
420 }
421 }
422 }
423 }
424 if !found {
425 t.Fatal("missing imposed configure")
426 }
427 w.AddFloating(2, 20, 10)
428 check(0.3)
429 found = false
430 for len(commands) > 0 {
431 if v, ok := (<-commands).(ports.ConfigureWindow); ok && v.ID == 2 {
432 found = true
433 if v.Width != 0 || v.Height != 0 {
434 t.Fatalf("native configure %+v", v)
435 }
436 }
437 }
438 if !found {
439 t.Fatal("missing native configure")
440 }
441 // Hiding the stash leaves the dialog, and its veil.
442 w.ToggleStashVisible()
443 check(0.3)
444 w.RemoveWindow(2)
445 check(0)
446 w.ToggleStashVisible()
447 check(0.3)
448 w.AddFloating(2, 20, 10)
449 w.SetFullscreen(1, true)
450 check(0)
451 w.SetFullscreen(1, false)
452 w.ToggleWindowStash()
453 w.SetFullscreen(1, true)
454 check(0)
455 // A dialog of a fullscreen tile waits hidden: no veil over the game.
456 w.SetFullscreen(1, false)
457 w.RemoveWindow(1)
458 w.RemoveWindow(2)
459 w.AddWindow(3)
460 w.SetFullscreen(3, true)
461 check(0)
462 w.AddFloating(4, 20, 10)
463 check(0)
464 w.SetFullscreen(3, false)
465 check(0.3)
466}
467
468// Activating a hidden stashed window shows the stash and focuses it; the
469// fullscreen window it hid behind leaves fullscreen.
470func TestActivateHiddenFloat(t *testing.T) {
471 w := workspace()
472 w.AddWindow(1)
473 w.AddWindow(2)
474 w.ToggleWindowStash()
475 w.ToggleStashVisible()
476 w.FocusID(1)
477 w.SetFullscreen(1, true)
478 w.Activate(2)
479 if id, _ := w.Focused(); id != 2 || w.stashHidden || w.fullscreen != 0 {
480 t.Fatalf("focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen)
481 }
482 // The float's own fullscreen, asked while hidden, comes back with it.
483 w.FocusID(2)
484 w.ToggleStashVisible()
485 w.SetFullscreen(2, true)
486 w.SetFullscreen(1, true)
487 w.Activate(2)
488 if id, _ := w.Focused(); id != 2 || w.stashHidden || w.fullscreen != 2 {
489 t.Fatalf("own fullscreen: focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen)
490 }
491}