1package core
2
3import (
4 "slices"
5 "testing"
6)
7
8func cascadeWorkspace(n int) *Workspace {
9 w := &Workspace{Overflow: OverflowCascade, MaxColumns: 3}
10 w.SetOutput(900, 600)
11 for i := 1; i <= n; i++ {
12 w.AddWindow(WindowID(i))
13 }
14 return w
15}
16
17func TestCascadeBandsAndReveal(t *testing.T) {
18 for _, n := range []int{0, 1, 3, 4, 7} {
19 w := cascadeWorkspace(n)
20 ps := w.Layout()
21 if len(ps) != n {
22 t.Fatalf("n=%d placements=%d", n, len(ps))
23 }
24 for i, p := range ps {
25 want := Rect{X: i % 3 * 300, Y: (i/3)*600 - w.View, W: 300, H: 600}
26 if p.Rect != want {
27 t.Fatalf("n=%d window=%d rect=%v want=%v", n, p.ID, p.Rect, want)
28 }
29 }
30 if n > 0 && w.View != ((n-1)/3)*600 {
31 t.Fatalf("n=%d view=%d", n, w.View)
32 }
33 }
34}
35
36func TestCascadeCompactionAndNavigation(t *testing.T) {
37 w := cascadeWorkspace(7)
38 w.FocusID(2)
39 if !w.FocusWindow(1) {
40 t.Fatal("no lower band")
41 }
42 if id, _ := w.Focused(); id != 5 {
43 t.Fatalf("focus=%d", id)
44 }
45 w.RemoveWindow(1)
46 if w.View != 600 {
47 t.Fatalf("compacted view=%d", w.View)
48 }
49 w.SetMaxColumns(2)
50 if w.View != 600 {
51 t.Fatalf("changed max view=%d", w.View)
52 }
53 w.FocusID(2)
54 w.AddWindow(8)
55 if w.Columns[len(w.Columns)-1].Windows[0] != 8 {
56 t.Fatal("new column did not append")
57 }
58 w.SetUsable(Rect{X: 0, Y: 20, W: 900, H: 580})
59 if w.View != w.band(w.Focus)*580 {
60 t.Fatal("resize did not reveal band")
61 }
62 if !slices.Equal(w.snapPoints(), []float64{0, 580, 1160, 1740}) {
63 t.Fatalf("points=%v", w.snapPoints())
64 }
65}
66
67func TestCascadeCovers(t *testing.T) {
68 w := cascadeWorkspace(4)
69 w.FocusID(2)
70 w.ToggleFullWidth()
71 ps := w.Layout()
72 for _, p := range ps {
73 if p.ID == 2 && p.Rect.W != 900 {
74 t.Fatalf("full width=%v", p.Rect)
75 }
76 if (p.ID == 1 || p.ID == 3) && !p.Hidden {
77 t.Fatalf("sibling %d visible", p.ID)
78 }
79 }
80 w.ToggleFullWidth()
81 w.ToggleFullscreen()
82 for _, p := range w.Layout() {
83 if p.ID == 2 && p.Rect != w.Output {
84 t.Fatalf("fullscreen=%v", p.Rect)
85 }
86 if p.ID != 2 && !p.Hidden {
87 t.Fatalf("fullscreen sibling %d visible", p.ID)
88 }
89 }
90 w.ToggleFullscreen()
91 w.FocusID(4)
92 if w.View != 600 {
93 t.Fatal("fullscreen restore lost band reveal")
94 }
95}
96
97func TestCascadeClearsMaximizationOnBandChanges(t *testing.T) {
98 for _, move := range []struct {
99 name string
100 run func(*Workspace)
101 }{
102 {"focus", func(w *Workspace) { w.FocusWindow(1) }},
103 {"append", func(w *Workspace) { w.AddWindow(5) }},
104 {"insert", func(w *Workspace) { w.insertColumn(4, Column{Windows: []WindowID{5}}) }},
105 } {
106 t.Run(move.name, func(t *testing.T) {
107 w := cascadeWorkspace(4)
108 w.FocusID(1)
109 w.ToggleFullWidth()
110 move.run(w)
111 for _, col := range w.Columns {
112 if col.FullWidth {
113 t.Fatal("stale maximization")
114 }
115 }
116 })
117 }
118}
119
120func TestCascadeReloadAndStashRestoreClearMaximization(t *testing.T) {
121 w := cascadeWorkspace(3)
122 w.FocusID(1)
123 w.ToggleFullWidth()
124 w.restore(4, &origPlace{stacked: []WindowID{2}, col: 1})
125 for _, c := range w.Columns {
126 if c.FullWidth {
127 t.Fatal("restore retained maximization")
128 }
129 }
130 m := NewMonitor()
131 m.SetOutput(900, 600)
132 for i := WindowID(1); i <= 3; i++ {
133 m.AddWindow(i)
134 }
135 m.Current().FocusID(1)
136 m.Current().ToggleFullWidth()
137 m.Current().FocusID(3)
138 m.SetOverflow(OverflowCascade)
139 for _, c := range m.Current().Columns {
140 if c.FullWidth {
141 t.Fatal("reload retained maximization")
142 }
143 }
144}
145
146func TestCascadeGestureValidityDoesNotAllocate(t *testing.T) {
147 m := NewMonitor()
148 m.SetOutput(900, 600)
149 m.SetOverflow(OverflowCascade)
150 for i := WindowID(1); i <= 7; i++ {
151 m.AddWindow(i)
152 }
153 g := &swipeGesture{ws: m.Current(), points: m.Current().snapPoints(), horizontal: false, opens: m.overviewOpens}
154 if n := testing.AllocsPerRun(100, func() {
155 if g.columnsChanged(m) {
156 t.Fatal("unchanged gesture invalidated")
157 }
158 }); n != 0 {
159 t.Fatalf("allocations=%v", n)
160 }
161}
162
163func TestCascadeGeometryReusesBuffer(t *testing.T) {
164 w := cascadeWorkspace(7)
165 buf := w.columnRectsInto(nil, false)
166 if n := testing.AllocsPerRun(100, func() { buf = w.columnRectsInto(buf, false) }); n != 0 {
167 t.Fatalf("allocations=%v", n)
168 }
169 // The overview's lane-encoded tiles reuse their buffer too.
170 tiles, _, _ := w.previewTiles()
171 if n := testing.AllocsPerRun(100, func() { tiles, _, _ = w.previewTilesInto(tiles[:0]) }); n != 0 {
172 t.Fatalf("preview tile allocations=%v", n)
173 }
174}
175
176// Lanes are the bands of a cascade workspace and the whole row of any other.
177func TestLaneGeometry(t *testing.T) {
178 w := cascadeWorkspace(7) // bands {0,1,2} {3,4,5} {6}
179 for _, c := range []struct{ i, lane int }{{0, 0}, {2, 0}, {3, 1}, {6, 2}} {
180 if got := w.laneOf(c.i); got != c.lane {
181 t.Fatalf("cascade laneOf(%d)=%d, want %d", c.i, got, c.lane)
182 }
183 }
184 for lane, want := range [][2]int{{0, 2}, {3, 5}, {6, 6}} {
185 if first, last := w.laneBounds(lane); first != want[0] || last != want[1] {
186 t.Fatalf("cascade laneBounds(%d)=%d,%d, want %v", lane, first, last, want)
187 }
188 }
189 for _, lane := range []int{-1, 3} {
190 if first, last := w.laneBounds(lane); first <= last {
191 t.Fatalf("cascade laneBounds(%d)=%d,%d exists", lane, first, last)
192 }
193 }
194 // The closest column of the adjacent lane keeps its offset, clamped.
195 for _, c := range []struct{ focus, d, want int }{{1, 1, 4}, {5, 1, 6}, {4, -1, 1}, {2, -1, -1}, {6, 1, -1}, {6, -1, 3}} {
196 w.Focus = c.focus
197 if got := w.laneNeighbor(c.d); got != c.want {
198 t.Fatalf("cascade laneNeighbor from %d by %d = %d, want %d", c.focus, c.d, got, c.want)
199 }
200 }
201
202 s := &Workspace{Overflow: OverflowScroll, MaxColumns: 3}
203 s.SetOutput(900, 600)
204 for i := 1; i <= 5; i++ {
205 s.AddWindow(WindowID(i))
206 }
207 for i := range s.Columns {
208 if s.laneOf(i) != 0 {
209 t.Fatalf("scroll laneOf(%d)=%d", i, s.laneOf(i))
210 }
211 }
212 if first, last := s.laneBounds(0); first != 0 || last != 4 {
213 t.Fatalf("scroll laneBounds(0)=%d,%d", first, last)
214 }
215 for _, lane := range []int{-1, 1} {
216 if first, last := s.laneBounds(lane); first <= last {
217 t.Fatalf("scroll laneBounds(%d)=%d,%d exists", lane, first, last)
218 }
219 }
220 for _, d := range []int{-1, 1} {
221 if got := s.laneNeighbor(d); got != -1 {
222 t.Fatalf("scroll laneNeighbor(%d)=%d", d, got)
223 }
224 }
225}
226
227func TestMixedLayoutSwitchKeepsSourceAxis(t *testing.T) {
228 m := NewMonitor()
229 m.SetOutput(900, 600)
230 m.Current().Overflow = OverflowCascade
231 m.AddWindow(1)
232 m.Focus(1)
233 m.AddWindow(2)
234 m.Current().Overflow = OverflowScroll
235 m.Focus(0)
236 m.switchAxis = m.Current().policy().workspace
237 m.switchView.off = .5
238 ps := m.layoutInto(nil)
239 for _, p := range ps {
240 if p.ID == 1 && (p.Rect.X != -450 || p.Rect.Y != 0) {
241 t.Fatalf("cascade source offset=%v", p.Rect)
242 }
243 }
244 m.stopSwitch()
245 m.Focus(1)
246 m.switchAxis = m.Current().policy().workspace
247 m.switchView.off = -.5
248 ps = m.layoutInto(nil)
249 for _, p := range ps {
250 if p.ID == 2 && (p.Rect.X != 0 || p.Rect.Y != 300) {
251 t.Fatalf("scroll source offset=%v", p.Rect)
252 }
253 }
254}
255
256func TestCascadeViewportSnap(t *testing.T) {
257 w := cascadeWorkspace(7)
258 w.FocusID(3)
259 if w.snapFocus(600, true) != 5 {
260 t.Fatalf("nearest cell=%d", w.snapFocus(600, true))
261 }
262 if w.snapFocus(1200, true) != 6 {
263 t.Fatal("short band neighbour")
264 }
265 w.SetUsable(Rect{})
266 if got := w.snapPoints(); len(got) != 1 || got[0] != 0 {
267 t.Fatalf("zero area=%v", got)
268 }
269}