1package core
2
3import (
4 "slices"
5 "testing"
6)
7
8func order(w *Workspace) []WindowID {
9 ids := []WindowID{}
10 for _, p := range w.Layout() {
11 if !p.Hidden {
12 ids = append(ids, p.ID)
13 }
14 }
15 return ids
16}
17
18func wantOrder(t *testing.T, w *Workspace, want ...WindowID) {
19 t.Helper()
20 if got := order(w); !slices.Equal(got, want) {
21 t.Fatalf("order %v, want %v", got, want)
22 }
23}
24
25func TestCoveringFloatOrder(t *testing.T) {
26 for _, overflow := range []Overflow{OverflowScroll, OverflowFixed} {
27 t.Run(string(overflow), func(t *testing.T) {
28 w := workspace()
29 w.Overflow = overflow
30 w.border = 2
31 w.AddWindow(1)
32 w.AddWindow(2)
33 w.AddFloating(3, 98, 79) // placed rect fills the 100x81 usable area
34 wantOrder(t, w, 1, 2, 3)
35 w.FocusID(2)
36 wantOrder(t, w, 3, 1, 2)
37 if id, _ := w.Focused(); id != 2 {
38 t.Fatalf("focus %d", id)
39 }
40 w.RemoveWindow(2)
41 wantOrder(t, w, 3, 1)
42 if id, _ := w.Focused(); id != 1 {
43 t.Fatalf("after close focus %d", id)
44 }
45 w.AddFloating(4, 20, 20) // new dialogs map on top
46 wantOrder(t, w, 3, 1, 4)
47 w.FocusID(3)
48 wantOrder(t, w, 1, 3, 4) // user-selected float rises only within its allowed group
49 w.FocusID(1)
50 wantOrder(t, w, 3, 1, 4)
51 w.ResizeFloating(3, 99, 80)
52 wantOrder(t, w, 3, 1, 4)
53 w.SetOutput(102, 83)
54 w.SetUsable(Rect{W: 102, H: 83})
55 wantOrder(t, w, 3, 1, 4)
56 w.ResizeFloating(3, 20, 20)
57 wantOrder(t, w, 1, 4, 3)
58 })
59 }
60}
61
62func TestCoveringFloatGeometryAndGroup(t *testing.T) {
63 w := workspace()
64 w.border = 2
65 w.AddWindow(1)
66 w.AddFloating(2, 94, 75) // two border widths plus two pixels on each axis
67 if !w.coversFloat(w.Floats[0]) {
68 t.Fatal("rounding tolerance")
69 }
70 w.FocusID(1)
71 wantOrder(t, w, 2, 1)
72 w.SetUsable(Rect{X: 5, Y: 4, W: 90, H: 73})
73 wantOrder(t, w, 2, 1)
74 w.SetUsable(Rect{W: 100, H: 81})
75 wantOrder(t, w, 2, 1)
76 w.border = 0
77 w.reconcileFloats()
78 wantOrder(t, w, 1, 2)
79}
80
81func TestDemotedFloatFullscreenRoundTrip(t *testing.T) {
82 m := monitor()
83 m.SetBorder(2)
84 for _, overflow := range []Overflow{OverflowScroll, OverflowFixed} {
85 m = monitor()
86 m.SetBorder(2)
87 m.Current().Overflow = overflow
88 m.AddWindow(1)
89 m.AddFloating(2, 100, 80)
90 w := m.Current()
91 w.FocusID(1)
92 wantOrder(t, w, 2, 1)
93 m.SetFullscreen(2, true)
94 // Fixed overflow refuses an unfocused window's fullscreen (ADR 011).
95 if p := placement(w, 2); p.Fullscreen != (overflow == OverflowScroll) {
96 t.Fatalf("%v: fullscreen %+v", overflow, p)
97 }
98 m.SetFullscreen(2, false)
99 wantOrder(t, w, 2, 1)
100 if id, _ := w.Focused(); id != 1 {
101 t.Fatalf("focus %d", id)
102 }
103 }
104}
105
106func TestDemotedFloatMoveAndAdopt(t *testing.T) {
107 m := monitor()
108 m.SetBorder(2)
109 m.AddWindow(1)
110 m.AddFloating(2, 100, 80)
111 w := m.Current()
112 w.FocusID(1)
113 other := monitor()
114 other.SetOutput(102, 82)
115 other.SetBorder(2)
116 m.take(w)
117 other.adopt(w, false, 0)
118 if !w.Floats[0].below {
119 t.Fatal("adopt raised float")
120 }
121 wantOrder(t, w, 2, 1)
122 // A user moving the selected float to a workspace raises it there.
123 other.show(w)
124 w.FocusID(2)
125 other.MoveToWorkspace(1, false)
126 if w.has(2) || !other.Workspaces[1].has(2) {
127 t.Fatal("move lost float")
128 }
129}
130
131// Up from the top of a column raises the last demoted covering float;
132// down leaves it for the columns again without changing workspaces.
133func TestFocusWindowReturnsToDemotedCoveringFloat(t *testing.T) {
134 for _, overflow := range []Overflow{OverflowScroll, OverflowFixed} {
135 t.Run(string(overflow), func(t *testing.T) {
136 m := monitor()
137 m.SetOverflow(overflow)
138 m.SetBorder(2)
139 m.AddWindow(1) // kitty
140 m.AddFloating(2, 100, 80) // covering game
141 m.AddFloating(3, 100, 80) // last demoted float is on top
142 w := m.Current()
143 w.FocusID(1)
144 wantOrder(t, w, 2, 3, 1)
145 m.Apply(ActionFocusWindowUp)
146 if id, _ := w.Focused(); id != 3 || m.Active != 0 || w.Floats[len(w.Floats)-1].below {
147 t.Fatalf("up focused %d on workspace %d, floats %+v", id, m.Active, w.Floats)
148 }
149 wantOrder(t, w, 2, 1, 3)
150 m.Apply(ActionFocusWindowDown)
151 if id, _ := w.Focused(); id != 1 || m.Active != 0 {
152 t.Fatalf("down focused %d on workspace %d", id, m.Active)
153 }
154 wantOrder(t, w, 2, 3, 1)
155 })
156 }
157}
158
159func TestFocusWindowDemotedFloatAfterColumnAndScreenNeighbors(t *testing.T) {
160 m := monitor()
161 m.SetOverflow(OverflowFixed)
162 m.SetBorder(2)
163 m.AddWindow(1)
164 m.AddWindow(2)
165 w := m.Current()
166 w.ConsumeOrExpel(-1) // two windows in the same column
167 w.AddFloating(9, 100, 80)
168 w.FocusID(1) // top of the column
169 w.FocusID(2) // bottom of the column
170 if !w.FocusWindow(-1) {
171 t.Fatal("up inside column failed")
172 }
173 if id, _ := w.Focused(); id != 1 || !w.Floats[0].below {
174 t.Fatalf("up inside column focused %d, float %+v", id, w.Floats[0])
175 }
176 if !w.FocusWindow(-1) {
177 t.Fatal("up to float failed")
178 }
179 if id, _ := w.Focused(); id != 9 {
180 t.Fatalf("up to float focused %d", id)
181 }
182
183 m = monitor()
184 m.SetOverflow(OverflowFixed)
185 m.SetMaxColumns(2)
186 for id := WindowID(1); id <= 4; id++ {
187 m.AddWindow(id)
188 }
189 w = m.Current()
190 w.AddFloating(9, 100, 80)
191 w.FocusID(4)
192 if !w.FocusWindow(-1) {
193 t.Fatal("up to on-screen column failed")
194 }
195 if id, _ := w.Focused(); id != 2 || !w.Floats[0].below {
196 t.Fatalf("up focused %d, float %+v", id, w.Floats[0])
197 }
198}
199
200func TestFocusWindowWithoutDemotedFloatChangesWorkspace(t *testing.T) {
201 m := monitor()
202 m.AddWindow(1)
203 m.Focus(1)
204 m.AddWindow(2)
205 m.Apply(ActionFocusWindowUp)
206 if m.Active != 0 {
207 t.Fatalf("up on workspace %d", m.Active)
208 }
209}
210
211// Up from a shown stash stays in the stash, even with a demoted covering
212// float; from a fullscreen float it leaves fullscreen for the tile below.
213func TestFocusWindowUpStashAndPinned(t *testing.T) {
214 m := monitor()
215 m.SetBorder(2)
216 m.AddWindow(1)
217 m.AddWindow(2)
218 w := m.Current()
219 w.AddFloating(9, 100, 80)
220 w.FocusID(2)
221 w.ToggleWindowStash() // 2 to the shown stash, focused
222 if !w.Floats[0].below {
223 t.Fatalf("float not below: %+v", w.Floats[0])
224 }
225 m.Apply(ActionFocusWindowUp)
226 if id, _ := w.Focused(); id != 2 || !w.Floats[0].below || m.Active != 0 {
227 t.Fatalf("stash up focused %d on %d, float %+v", id, m.Active, w.Floats[0])
228 }
229
230 m = monitor()
231 m.SetOverflow(OverflowFixed)
232 m.AddWindow(1)
233 m.AddFloating(2, 50, 40)
234 m.SetFullscreen(2, true)
235 if m.Active != 0 || !m.Current().pinned() {
236 t.Fatalf("setup: active %d", m.Active)
237 }
238 m.Apply(ActionFocusWindowUp)
239 if id, _ := m.Current().Focused(); id != 1 || m.Active != 0 || m.Current().fullscreen != 0 {
240 t.Fatalf("pinned up focused %d on %d", id, m.Active)
241 }
242}
243
244// Directional focus on a covering float with nothing under it keeps the
245// float focused.
246func TestCoveringFloatAloneKeepsFocus(t *testing.T) {
247 w := workspace()
248 w.border = 2
249 w.AddFloating(3, 98, 79)
250 w.FocusColumn(-1)
251 w.FocusWindow(1)
252 if id, ok := w.Focused(); !ok || id != 3 {
253 t.Fatalf("focus %d %v", id, ok)
254 }
255 wantOrder(t, w, 3)
256}
257
258// A tile leaving fullscreen with the focus comes back in front of a
259// covering float mapped meanwhile.
260func TestFullscreenReturnRaisesColumns(t *testing.T) {
261 for _, stacked := range []bool{false, true} {
262 m := monitor()
263 m.SetBorder(2)
264 m.SetOverflow(OverflowFixed)
265 m.AddWindow(1)
266 m.AddWindow(2)
267 if stacked {
268 m.Current().ConsumeOrExpel(-1)
269 }
270 w := m.Current()
271 w.FocusID(2)
272 m.ToggleFullscreen()
273 w.AddFloating(3, 100, 80)
274 m.ToggleFullscreen() // back, focused
275 if id, _ := w.Focused(); id != 2 {
276 t.Fatalf("stacked %v: focus %d", stacked, id)
277 }
278 if got := order(w); got[0] != 3 {
279 t.Fatalf("stacked %v: order %v, covering float not below", stacked, got)
280 }
281 }
282}