1package core
2
3import (
4 "slices"
5 "testing"
6)
7
8// cascadeMovesCore is a core whose focused screen holds a cascade of
9// windows 1..n in bands of maxCols. With sides, screens L and R sit on
10// either side of it; otherwise it is alone.
11func cascadeMovesCore(t *testing.T, n, maxCols int, sides bool) (*Core, *Workspace) {
12 t.Helper()
13 tiles := []tile{{"M", 900, 0, 900, 600}}
14 focus := 0
15 if sides {
16 tiles = []tile{{"L", 0, 0, 900, 600}, tiles[0], {"R", 1800, 0, 900, 600}}
17 focus = 1
18 }
19 c := layoutCore(focus, tiles...)
20 w := c.cur().mon.Current()
21 w.Overflow = OverflowCascade
22 w.SetMaxColumns(maxCols)
23 for id := WindowID(1); id <= WindowID(n); id++ {
24 w.AddWindow(id)
25 }
26 c.cur().mon.normalize() // the trailing empty workspace, as in a session
27 return c, w
28}
29
30// columnOrder is the first window of each column, in column order.
31func columnOrder(w *Workspace) []WindowID {
32 var ids []WindowID
33 for _, c := range w.Columns {
34 ids = append(ids, c.Windows[0])
35 }
36 return ids
37}
38
39func screenWorkspace(c *Core, name string) *Workspace {
40 for _, s := range c.screens {
41 if s.name() == name {
42 return s.mon.Current()
43 }
44 }
45 return nil
46}
47
48func wantColumns(t *testing.T, w *Workspace, want ...WindowID) {
49 t.Helper()
50 if got := columnOrder(w); !slices.Equal(got, want) {
51 t.Fatalf("columns %v, want %v", got, want)
52 }
53}
54
55func wantFocus(t *testing.T, c *Core, screen string, id WindowID) {
56 t.Helper()
57 if got := c.cur().name(); got != screen {
58 t.Fatalf("focused screen %q, want %q", got, screen)
59 }
60 if got, _ := c.cur().mon.Current().Focused(); got != id {
61 t.Fatalf("focused window %d, want %d", got, id)
62 }
63}
64
65// At the edge of its band, a cascade column goes to the neighbor monitor
66// on that side, and focus with it; bands {1 2 3} {4 5}.
67func TestCascadeBandEdgeMovesToNeighborMonitor(t *testing.T) {
68 for _, tc := range []struct {
69 name string
70 from WindowID
71 action Action
72 screen string
73 here, moved []WindowID
74 focusedAfter WindowID
75 }{
76 {"move left from band start", 4, ActionMoveColumnLeft, "L", []WindowID{1, 2, 3, 5}, []WindowID{4}, 4},
77 {"move right from band end", 3, ActionMoveColumnRight, "R", []WindowID{1, 2, 4, 5}, []WindowID{3}, 3},
78 {"move right from last column", 5, ActionMoveColumnRight, "R", []WindowID{1, 2, 3, 4}, []WindowID{5}, 5},
79 {"focus left from band start", 4, ActionFocusColumnLeft, "L", []WindowID{1, 2, 3, 4, 5}, nil, 0},
80 {"focus right from band end", 3, ActionFocusColumnRight, "R", []WindowID{1, 2, 3, 4, 5}, nil, 0},
81 } {
82 t.Run(tc.name, func(t *testing.T) {
83 c, w := cascadeMovesCore(t, 5, 3, true)
84 w.FocusID(tc.from)
85 c.applyAction(tc.action)
86 wantColumns(t, w, tc.here...)
87 wantColumns(t, screenWorkspace(c, tc.screen), tc.moved...)
88 wantFocus(t, c, tc.screen, tc.focusedAfter)
89 })
90 }
91}
92
93// Inside a band, left/right stay on the screen.
94func TestCascadeInsideBandStaysOnMonitor(t *testing.T) {
95 c, w := cascadeMovesCore(t, 5, 3, true)
96 w.FocusID(5)
97 c.applyAction(ActionMoveColumnLeft)
98 wantColumns(t, w, 1, 2, 3, 5, 4)
99 wantFocus(t, c, "M", 5)
100 c.applyAction(ActionFocusColumnRight)
101 wantFocus(t, c, "M", 4)
102}
103
104// With no monitor on that side, left/right never cross into the neighbor
105// band: at the band edge they step the workspace (the first has none before
106// it; the last is the trailing empty one). up/down cross bands, and
107// move-column still crosses in column order.
108func TestCascadeBandEdgeWithoutMonitorFocusStepsMoveCrosses(t *testing.T) {
109 c, w := cascadeMovesCore(t, 5, 3, false)
110 m := c.cur().mon
111 w.FocusID(4)
112 c.applyAction(ActionFocusColumnLeft)
113 wantFocus(t, c, "M", 4)
114 if m.Active != 0 {
115 t.Fatalf("active %d, want 0", m.Active)
116 }
117 w.FocusID(3)
118 c.applyAction(ActionFocusColumnRight)
119 if m.Active != 1 || m.Current() == w {
120 t.Fatalf("active %d, want the trailing empty workspace", m.Active)
121 }
122 m.Focus(0)
123 w.FocusID(4)
124 c.applyAction(ActionMoveColumnLeft)
125 wantColumns(t, w, 1, 2, 4, 3, 5)
126 wantFocus(t, c, "M", 4)
127 w.FocusID(4)
128 c.applyAction(ActionMoveColumnRight)
129 wantColumns(t, w, 1, 2, 3, 4, 5)
130 wantFocus(t, c, "M", 4)
131}
132
133// With one column per band, every column is at both edges of its band.
134func TestCascadeOneColumnBandsLeaveForMonitor(t *testing.T) {
135 c, w := cascadeMovesCore(t, 2, 1, true)
136 w.FocusID(2)
137 c.applyAction(ActionMoveColumnLeft)
138 wantColumns(t, w, 1)
139 wantColumns(t, screenWorkspace(c, "L"), 2)
140 wantFocus(t, c, "L", 2)
141}
142
143// Fixed overflow keeps its column order edge: a non-edge column swaps.
144func TestFixedMoveColumnKeepsLinearEdge(t *testing.T) {
145 c, w := cascadeMovesCore(t, 2, 3, true)
146 w.Overflow = OverflowFixed
147 w.FocusID(2)
148 c.applyAction(ActionMoveColumnLeft)
149 wantColumns(t, w, 2, 1)
150 wantFocus(t, c, "M", 2)
151}
152
153// At the top or bottom of its column, move-window-up/down moves a cascade
154// column to the adjacent band, where focus-window-up/down goes; bands
155// {1 2 3} {4 5}.
156func TestCascadeMoveWindowCrossesBands(t *testing.T) {
157 c, w := cascadeMovesCore(t, 5, 3, false)
158 w.FocusID(5)
159 c.applyAction(ActionMoveWindowUp)
160 wantColumns(t, w, 1, 5, 3, 4, 2)
161 wantFocus(t, c, "M", 5)
162 c.applyAction(ActionMoveWindowDown)
163 wantColumns(t, w, 1, 2, 3, 4, 5)
164 wantFocus(t, c, "M", 5)
165}
166
167// The last band is partial: the move clamps to its last column, and going
168// back up keeps the new offset, as focus does.
169func TestCascadeMoveWindowPartialBand(t *testing.T) {
170 c, w := cascadeMovesCore(t, 5, 3, false)
171 w.FocusID(3)
172 c.applyAction(ActionMoveWindowDown)
173 wantColumns(t, w, 1, 2, 5, 4, 3)
174 c.applyAction(ActionMoveWindowUp)
175 wantColumns(t, w, 1, 3, 5, 4, 2)
176 wantFocus(t, c, "M", 3)
177}
178
179// No band past the first or last: nothing moves.
180func TestCascadeMoveWindowStopsAtOuterBands(t *testing.T) {
181 c, w := cascadeMovesCore(t, 5, 3, false)
182 w.FocusID(1)
183 c.applyAction(ActionMoveWindowUp)
184 wantFocus(t, c, "M", 1)
185 w.FocusID(4)
186 c.applyAction(ActionMoveWindowDown)
187 wantColumns(t, w, 1, 2, 3, 4, 5)
188 wantFocus(t, c, "M", 4)
189}
190
191// A maximized column leaving its band gives up maximization: the band it
192// joins shows all its columns.
193func TestCascadeMoveWindowEndsMaximization(t *testing.T) {
194 c, w := cascadeMovesCore(t, 5, 3, false)
195 w.FocusID(5)
196 w.ToggleFullWidth()
197 c.applyAction(ActionMoveWindowUp)
198 wantColumns(t, w, 1, 5, 3, 4, 2)
199 if w.Columns[1].FullWidth || len(w.maximized) != 0 {
200 t.Fatalf("still maximized: full width %t, MRU %v", w.Columns[1].FullWidth, w.maximized)
201 }
202}
203
204// A fullscreen tile at its band edge leaves for the neighbor monitor and
205// stays fullscreen, as a tile there does.
206func TestCascadeFullscreenBandEdgeFocusesMonitor(t *testing.T) {
207 c, w := cascadeMovesCore(t, 5, 3, true)
208 w.FocusID(3)
209 w.ToggleFullscreen()
210 c.applyAction(ActionFocusColumnRight)
211 if got := c.cur().name(); got != "R" {
212 t.Fatalf("focused screen %q, want R", got)
213 }
214 if id, _ := w.Focused(); w.fullscreen != 3 || id != 3 {
215 t.Fatalf("fullscreen %d, focused %d on M, want 3 and 3", w.fullscreen, id)
216 }
217}
218
219// From a native float over the columns, the first move leaves the float
220// on this monitor, even with the focused column at its band edge.
221func TestCascadeFloatAtBandEdgeStaysOnMonitor(t *testing.T) {
222 c, w := cascadeMovesCore(t, 5, 3, true)
223 w.FocusID(3)
224 w.AddFloating(9, 100, 100)
225 c.applyAction(ActionFocusColumnRight)
226 if got := c.cur().name(); got != "M" {
227 t.Fatalf("focused screen %q, want M", got)
228 }
229 if w.floatFocus {
230 t.Fatal("focus still on the float")
231 }
232}
233
234// A stacked window moves inside its column first, then the column changes
235// band.
236func TestCascadeMoveWindowStackedFirst(t *testing.T) {
237 c, w := cascadeMovesCore(t, 5, 3, false)
238 w.FocusID(5)
239 w.ConsumeOrExpel(-1) // bands {1 2 3} {4/5}
240 if got := w.Columns[3].Windows; !slices.Equal(got, []WindowID{4, 5}) {
241 t.Fatalf("stack %v, want [4 5]", got)
242 }
243 c.applyAction(ActionMoveWindowUp)
244 if got := w.Columns[3].Windows; !slices.Equal(got, []WindowID{5, 4}) {
245 t.Fatalf("stack %v, want [5 4]", got)
246 }
247 c.applyAction(ActionMoveWindowUp)
248 wantColumns(t, w, 5, 2, 3, 1)
249 wantFocus(t, c, "M", 5)
250}
251
252// A fullscreen window keeps its band.
253func TestCascadeMoveWindowFullscreenStays(t *testing.T) {
254 c, w := cascadeMovesCore(t, 5, 3, false)
255 w.FocusID(4)
256 w.ToggleFullscreen()
257 c.applyAction(ActionMoveWindowUp)
258 wantColumns(t, w, 1, 2, 3, 4, 5)
259}
260
261// Scroll overflow has no bands: up/down stop at the column edge.
262func TestScrollMoveWindowStopsAtColumnEdge(t *testing.T) {
263 c, w := cascadeMovesCore(t, 2, 3, false)
264 w.Overflow = OverflowScroll
265 w.FocusID(2)
266 c.applyAction(ActionMoveWindowUp)
267 c.applyAction(ActionMoveWindowDown)
268 wantColumns(t, w, 1, 2)
269 wantFocus(t, c, "M", 2)
270}