transition_stash_internal_test.go RawHistoryFolder
1package core
2
3import (
4 "math"
5 "slices"
6 "testing"
7 "time"
8
9 "github.com/bnema/neferwl/internal/ports"
10)
11
12// stashRig is indicatorCore with animations on and a stash of windows 4, 3
13// and 2 (2 selected, 3 peeking on its left) over the tile 1, settled. The
14// peek veil is 0.5.
15func stashRig(t *testing.T, on bool) (*Core, *indicatorClock, *screen, chan ports.ClientCommand) {
16 t.Helper()
17 c, ic, cmds := fadeCore(t)
18 c.cfg.Animations.On = on
19 c.cfg.Stash.Dim = 0.5
20 sc := c.cur()
21 for id := WindowID(1); id <= 4; id++ {
22 sc.mon.AddWindow(id)
23 }
24 for _, id := range []WindowID{4, 3, 2} {
25 sc.mon.Current().FocusID(id)
26 c.applyAction(ActionToggleWindowStash)
27 }
28 settleShown(t, c)
29 drainConfigures(cmds)
30 return c, ic, sc, cmds
31}
32
33// act is a bind: snapshot, action, transition.
34func act(c *Core, ic *indicatorClock, a Action) {
35 shots := c.snapshot(ic.now)
36 c.applyAction(a)
37 c.transition(shots, ic.now)
38}
39
40// Showing the stash fades in and grows every window it brings on screen;
41// the windows it keeps off screen stay out; the settled layout is the
42// final one at once, with no extra configure.
43func TestStashShowAppears(t *testing.T) {
44 c, ic, sc, cmds := stashRig(t, true)
45 act(c, ic, ActionToggleStashVisible)
46 settleShown(t, c)
47 if !sc.mon.Current().stashHidden {
48 t.Fatal("setup: the stash is shown")
49 }
50 ic.now = ic.now.Add(5 * time.Second)
51 c.animate(ic.now, nil)
52 settleShown(t, c)
53 drainConfigures(cmds)
54
55 act(c, ic, ActionToggleStashVisible)
56 s := indicatorScene(t, c)
57 for _, id := range []WindowID{2, 3} {
58 p := settledOf(t, sc, id)
59 w := sceneWindow(t, s, id)
60 if w.Hidden || w.Fade != 1 || w.Rect != scaledRect(p.Rect, appearScale) || w.Zoom < 0.85 || w.Zoom > 0.95 {
61 t.Fatalf("window %d first frame %+v, want invisible at 90 %% of %+v", id, w, p.Rect)
62 }
63 }
64 if w := sceneWindow(t, s, 4); !w.Hidden || w.Fade != 0 {
65 t.Fatalf("window 4 stays off screen: %+v", w)
66 }
67 // The action configures what it changes once, at the settled size.
68 cfgs := configuresOf(cmds)
69 for _, id := range []WindowID{2, 3} {
70 if len(cfgs[id]) != 1 {
71 t.Fatalf("window %d configured %d times, want once: %+v", id, len(cfgs[id]), cfgs[id])
72 }
73 want := c.clientRect(settledOf(t, sc, id))
74 if v := cfgs[id][0]; !v.Visible || v.Width != want.W || v.Height != want.H {
75 t.Fatalf("window %d configure %+v, want %dx%d (the settled client rect)", id, v, want.W, want.H)
76 }
77 }
78
79 s = frame(t, c, ic, 40*time.Millisecond)
80 for _, id := range []WindowID{2, 3} {
81 w := sceneWindow(t, s, id)
82 if !(w.Fade > 0 && w.Fade < 1) || !(w.Zoom > 0.9 && w.Zoom < 1) {
83 t.Fatalf("window %d mid-way fade %v zoom %v, want strictly between", id, w.Fade, w.Zoom)
84 }
85 }
86 s = frame(t, c, ic, 5*time.Second)
87 for _, id := range []WindowID{2, 3} {
88 p := settledOf(t, sc, id)
89 if w := sceneWindow(t, s, id); w.Fade != 0 || w.Zoom != 0 || w.Rect != p.Rect {
90 t.Fatalf("window %d settled %+v, want %+v", id, w, p.Rect)
91 }
92 }
93 if c.animating() || len(sc.rects) != 0 {
94 t.Fatalf("still animating: %d rects", len(sc.rects))
95 }
96 if ids := drainConfigures(cmds); len(ids) != 0 {
97 t.Fatalf("the animation reconfigured %v", ids)
98 }
99}
100
101// Hiding the stash keeps its windows drawn while they fade out and shrink,
102// out of reach of input and focus, until they settle.
103func TestStashHideLeaves(t *testing.T) {
104 c, ic, sc, cmds := stashRig(t, true)
105 left := map[WindowID]Placement{}
106 for _, id := range []WindowID{2, 3} {
107 left[id] = settledOf(t, sc, id)
108 }
109 act(c, ic, ActionToggleStashVisible)
110 s := indicatorScene(t, c)
111 if len(sc.settledLayout) != len(sc.shown) {
112 t.Fatalf("settled %d shown %d", len(sc.settledLayout), len(sc.shown))
113 }
114 for _, id := range []WindowID{2, 3} {
115 w := sceneWindow(t, s, id)
116 if w.Hidden || w.Fade != 0 || w.Rect != left[id].Rect {
117 t.Fatalf("window %d first frame %+v, want drawn opaque at %+v", id, w, left[id].Rect)
118 }
119 if _, _, ok := c.windowRect(id); ok {
120 t.Fatalf("window %d is still on screen for input", id)
121 }
122 }
123 // The selected window is under the tile: its centre hits the tile.
124 r := left[2].Rect
125 if id, _, _ := c.hit(float64(r.X+r.W/2), float64(r.Y+r.H/2)); id == 2 || id == 3 {
126 t.Fatalf("hit %d on a leaving window", id)
127 }
128 if id, _ := sc.mon.Focused(); id == 2 || id == 3 {
129 t.Fatalf("focused %d", id)
130 }
131 if ids := drainConfigures(cmds); slices.Contains(ids, 2) || slices.Contains(ids, 3) {
132 t.Fatalf("a leaving window was configured: %v", ids)
133 }
134
135 s = frame(t, c, ic, 40*time.Millisecond)
136 for _, id := range []WindowID{2, 3} {
137 if w := sceneWindow(t, s, id); !(w.Fade > 0 && w.Fade < 1) || !(w.Rect.W < left[id].Rect.W) {
138 t.Fatalf("window %d mid-way %+v", id, w)
139 }
140 }
141 s = frame(t, c, ic, 5*time.Second)
142 for _, id := range []WindowID{2, 3} {
143 if w := sceneWindow(t, s, id); !w.Hidden {
144 t.Fatalf("window %d still drawn after the settle: %+v", id, w)
145 }
146 }
147 if c.animating() || len(sc.rects) != 0 {
148 t.Fatalf("still animating: %d rects", len(sc.rects))
149 }
150 // The hide's own configure comes once the fade is over, from the size
151 // the window had (not from nothing).
152 var hidden []ports.ConfigureWindow
153 for {
154 select {
155 case cmd := <-cmds:
156 if v, ok := cmd.(ports.ConfigureWindow); ok && v.ID == 2 {
157 hidden = append(hidden, v)
158 }
159 continue
160 default:
161 }
162 break
163 }
164 if len(hidden) != 1 || hidden[0].Visible || hidden[0].Activated {
165 t.Fatalf("configures of window 2 after the hide: %+v", hidden)
166 }
167}
168
169// With animations off both are instant.
170func TestStashToggleOffIsInstant(t *testing.T) {
171 c, ic, sc, _ := stashRig(t, false)
172 act(c, ic, ActionToggleStashVisible)
173 s := indicatorScene(t, c)
174 if w := sceneWindow(t, s, 2); !w.Hidden || len(sc.rects) != 0 || c.animating() {
175 t.Fatalf("hide with animations off: %+v, %d rects", w, len(sc.rects))
176 }
177 act(c, ic, ActionToggleStashVisible)
178 s = indicatorScene(t, c)
179 if w := sceneWindow(t, s, 2); w.Hidden || w.Fade != 0 || w.Zoom != 0 || len(sc.rects) != 0 || c.animating() {
180 t.Fatalf("show with animations off: %+v, %d rects", w, len(sc.rects))
181 }
182}
183
184// Navigating in the stash animates the peek veil: the window that leaves the
185// centre darkens from 0, the one that reaches it clears from the veil.
186func TestStashNavigationAnimatesPeekDim(t *testing.T) {
187 c, ic, sc, _ := stashRig(t, true)
188 if w := sceneWindow(t, indicatorScene(t, c), 3); w.Dim != 0.5 {
189 t.Fatalf("setup: peek dim %v", w.Dim)
190 }
191 act(c, ic, ActionFocusColumnLeft) // 3 selected, 2 peeks on its right
192 s := indicatorScene(t, c)
193 if w := sceneWindow(t, s, 3); w.Dim != 0.5 || w.Focused != true {
194 t.Fatalf("window 3 right after: %+v, want the veil it had", w)
195 }
196 if w := sceneWindow(t, s, 2); w.Dim != 0 {
197 t.Fatalf("window 2 right after: dim %v, want 0 (it was selected)", w.Dim)
198 }
199 s = frame(t, c, ic, 40*time.Millisecond)
200 if w := sceneWindow(t, s, 3); !(w.Dim > 0 && w.Dim < 0.5) {
201 t.Fatalf("window 3 mid-way dim %v, want strictly between 0 and 0.5", w.Dim)
202 }
203 if w := sceneWindow(t, s, 2); !(w.Dim > 0 && w.Dim < 0.5) {
204 t.Fatalf("window 2 mid-way dim %v, want strictly between 0 and 0.5", w.Dim)
205 }
206 s = frame(t, c, ic, 5*time.Second)
207 if w := sceneWindow(t, s, 3); w.Dim != 0 {
208 t.Fatalf("window 3 settled dim %v, want 0", w.Dim)
209 }
210 if w := sceneWindow(t, s, 2); w.Dim != 0.5 {
211 t.Fatalf("window 2 settled dim %v, want 0.5", w.Dim)
212 }
213 if c.animating() || len(sc.rects) != 0 {
214 t.Fatalf("still animating: %d rects", len(sc.rects))
215 }
216}
217
218// A navigation that chains on a running one starts the veil where it was.
219func TestStashNavigationChainsPeekDim(t *testing.T) {
220 c, ic, _, _ := stashRig(t, true)
221 act(c, ic, ActionFocusColumnLeft)
222 indicatorScene(t, c)
223 ic.now = ic.now.Add(30 * time.Millisecond)
224 c.animate(ic.now, nil)
225 mid := sceneWindow(t, indicatorScene(t, c), 3).Dim
226 act(c, ic, ActionFocusColumnRight) // 3 goes back to a peek
227 if got := sceneWindow(t, indicatorScene(t, c), 3).Dim; got < mid-0.02 || got > mid+0.02 {
228 t.Fatalf("window 3 veil %v after the chained action, want the %v it had", got, mid)
229 }
230}
231
232// The overview pile: the stash cards shrink from the stash windows' rects
233// and settle on the pile layout (C1's rules).
234func TestOverviewPileScalesFromStash(t *testing.T) {
235 c, ic, sc, _ := stashRig(t, true)
236 from := map[WindowID]ports.Rect{}
237 s := indicatorScene(t, c)
238 for _, id := range []WindowID{2, 3} {
239 from[id] = sceneWindow(t, s, id).Rect
240 }
241 toggleOverview(c, sc, ic.now)
242 s = indicatorScene(t, c)
243 for _, id := range []WindowID{2, 3} {
244 w := sceneWindow(t, s, id)
245 p := settledOf(t, sc, id)
246 if w.Hidden || w.Preview != overviewCardZoom || w.Rect != from[id] || w.Zoom != 1 {
247 t.Fatalf("window %d first frame %+v, want its stash rect %+v at content scale 1 (card %+v)", id, w, from[id], p.Rect)
248 }
249 checkContent(t, w, from[id], "first frame")
250 }
251 s = frame(t, c, ic, 40*time.Millisecond)
252 for _, id := range []WindowID{2, 3} {
253 w := sceneWindow(t, s, id)
254 p := settledOf(t, sc, id)
255 if !(w.Rect.W < from[id].W && w.Rect.W > p.Rect.W) || !(w.Zoom > overviewCardZoom && w.Zoom < 1) {
256 t.Fatalf("window %d mid-way %+v, want between %+v and %+v", id, w, from[id], p.Rect)
257 }
258 checkContent(t, w, from[id], "mid-way")
259 }
260 s = frame(t, c, ic, 5*time.Second)
261 for _, id := range []WindowID{2, 3} {
262 w := sceneWindow(t, s, id)
263 p := settledOf(t, sc, id)
264 if w.Rect != p.Rect || w.Zoom != 0 || w.Fade != 0 || w.Preview != overviewCardZoom {
265 t.Fatalf("window %d settled %+v, want the pile card %+v", id, w, p)
266 }
267 }
268}
269
270// A navigation that brings a window to a margin makes it appear, one that
271// sends a peek off screen makes it leave; the others only change their veil
272// and rect.
273func TestStashNavigationAppearsAndLeaves(t *testing.T) {
274 c, ic, sc, cmds := stashRig(t, true)
275 w := sc.mon.Current()
276 w.AddWindow(5)
277 w.stashFocus = false // the new tile has the focus, not the stash
278 c.applyAction(ActionToggleWindowStash) // stash 4 3 2 5: 5 selected, 2 peeks
279 if len(w.Stash) != 4 || w.stashAt != 3 {
280 t.Fatalf("setup: stash %+v at %d", w.Stash, w.stashAt)
281 }
282 settleShown(t, c)
283 act(c, ic, ActionFocusColumnLeft) // 2 selected, 3 comes in, 5 stays a peek
284 s := indicatorScene(t, c)
285 if w := sceneWindow(t, s, 3); w.Hidden || w.Fade != 1 {
286 t.Fatalf("window 3 comes to the margin: %+v, want invisible at first", w)
287 }
288 if w := sceneWindow(t, s, 5); w.Hidden || w.Fade != 0 {
289 t.Fatalf("window 5 stays a peek: %+v", w)
290 }
291 settledAll := frame(t, c, ic, 5*time.Second)
292 if w := sceneWindow(t, settledAll, 3); w.Fade != 0 || w.Hidden {
293 t.Fatalf("window 3 settled %+v", w)
294 }
295 drainConfigures(cmds)
296 act(c, ic, ActionFocusColumnLeft) // 3 selected, 4 comes in, 5 goes
297 s = indicatorScene(t, c)
298 if w := sceneWindow(t, s, 4); w.Hidden || w.Fade != 1 {
299 t.Fatalf("window 4 comes to the margin: %+v", w)
300 }
301 if w := sceneWindow(t, s, 5); w.Hidden || w.Fade != 0 || w.Rect.W == 0 {
302 t.Fatalf("window 5 leaves from its peek rect: %+v", w)
303 }
304 s = frame(t, c, ic, 5*time.Second)
305 if w := sceneWindow(t, s, 5); !w.Hidden {
306 t.Fatalf("window 5 still drawn after the settle: %+v", w)
307 }
308 if c.animating() {
309 t.Fatal("still animating")
310 }
311}
312
313// near fails the test unless a and b differ by less than eps.
314func near(t *testing.T, what string, a, b, eps float64) {
315 t.Helper()
316 if math.Abs(a-b) > eps {
317 t.Fatalf("%s jumped: %v then %v", what, a, b)
318 }
319}
320
321// A hide during the show's fade-in continues from what was drawn: no jump
322// in fade or rect, then it settles gone.
323func TestStashHideDuringShowDoesNotJump(t *testing.T) {
324 c, ic, sc, _ := stashRig(t, true)
325 act(c, ic, ActionToggleStashVisible)
326 indicatorScene(t, c)
327 ic.now = ic.now.Add(5 * time.Second)
328 c.animate(ic.now, nil)
329 indicatorScene(t, c)
330 act(c, ic, ActionToggleStashVisible) // show
331 indicatorScene(t, c)
332 s := frame(t, c, ic, 30*time.Millisecond)
333 last := sceneWindow(t, s, 2)
334 if !(last.Fade > 0 && last.Fade < 1) {
335 t.Fatalf("setup: fade %v", last.Fade)
336 }
337 act(c, ic, ActionToggleStashVisible) // hide, same instant
338 w := sceneWindow(t, indicatorScene(t, c), 2)
339 if w.Hidden {
340 t.Fatalf("window 2 vanished: %+v", w)
341 }
342 near(t, "fade", last.Fade, w.Fade, 0.02)
343 near(t, "width", float64(last.Rect.W), float64(w.Rect.W), 2)
344 near(t, "x", float64(last.Rect.X), float64(w.Rect.X), 2)
345 near(t, "zoom", contentScale(last), contentScale(w), 0.03)
346 s = frame(t, c, ic, 5*time.Second)
347 if w := sceneWindow(t, s, 2); !w.Hidden || c.animating() || len(sc.rects) != 0 {
348 t.Fatalf("after the settle: %+v, %d rects", w, len(sc.rects))
349 }
350}
351
352// A show during the hide's fade-out continues from what was drawn.
353func TestStashShowDuringHideDoesNotJump(t *testing.T) {
354 c, ic, sc, _ := stashRig(t, true)
355 act(c, ic, ActionToggleStashVisible) // hide
356 indicatorScene(t, c)
357 s := frame(t, c, ic, 30*time.Millisecond)
358 last := sceneWindow(t, s, 2)
359 if !(last.Fade > 0 && last.Fade < 1) {
360 t.Fatalf("setup: fade %v", last.Fade)
361 }
362 act(c, ic, ActionToggleStashVisible) // show
363 w := sceneWindow(t, indicatorScene(t, c), 2)
364 if w.Hidden {
365 t.Fatalf("window 2 hidden: %+v", w)
366 }
367 near(t, "fade", last.Fade, w.Fade, 0.02)
368 near(t, "width", float64(last.Rect.W), float64(w.Rect.W), 2)
369 near(t, "x", float64(last.Rect.X), float64(w.Rect.X), 2)
370 near(t, "zoom", contentScale(last), contentScale(w), 0.03)
371 // The entrance now runs back to opaque (the fade's speed carries over:
372 // it may still rise a few frames before it turns).
373 s = frame(t, c, ic, 150*time.Millisecond)
374 if n := sceneWindow(t, s, 2); !(n.Fade < w.Fade) {
375 t.Fatalf("fade %v then %v, want it to clear", w.Fade, n.Fade)
376 }
377 s = frame(t, c, ic, 5*time.Second)
378 p := settledOf(t, sc, 2)
379 if w := sceneWindow(t, s, 2); w.Fade != 0 || w.Zoom != 0 || w.Rect != p.Rect || c.animating() {
380 t.Fatalf("settled %+v, want %+v", w, p.Rect)
381 }
382}
383
384// A peek sliding to a margin that a fast second navigation sends off screen
385// leaves from where it was drawn, mid-slide, not from its settled rect.
386func TestStashNavigationLeavesFromSlide(t *testing.T) {
387 c, ic, sc, _ := stashRig(t, true)
388 // 2 selected, 3 peeks left. Left: 3 centred, 2 peeks right, 4 enters.
389 act(c, ic, ActionFocusColumnLeft)
390 indicatorScene(t, c)
391 s := frame(t, c, ic, 30*time.Millisecond)
392 last := sceneWindow(t, s, 2)
393 settled := settledOf(t, sc, 2)
394 if last.Rect == settled.Rect {
395 t.Fatal("setup: window 2 already at rest")
396 }
397 // Left again: 4 centred, 3 peeks right, 2 goes off.
398 act(c, ic, ActionFocusColumnLeft)
399 w := sceneWindow(t, indicatorScene(t, c), 2)
400 if w.Hidden {
401 t.Fatalf("window 2 vanished: %+v", w)
402 }
403 near(t, "x", float64(last.Rect.X), float64(w.Rect.X), 2)
404 near(t, "width", float64(last.Rect.W), float64(w.Rect.W), 2)
405 near(t, "dim", last.Dim, w.Dim, 0.03)
406 if s = frame(t, c, ic, 5*time.Second); !sceneWindow(t, s, 2).Hidden {
407 t.Fatal("window 2 still drawn after the settle")
408 }
409}
410
411// Leaving entries land in the same order whatever the map's: a hidden stash
412// float (replaced in place) and absent tiles and floats (ID order, tiles
413// before the first float, floats last).
414func TestWithLeavingOrderIsDeterministic(t *testing.T) {
415 c, ic := indicatorCore(t)
416 c.cfg.Animations.On = true
417 sc := c.cur()
418 w := sc.mon.Current()
419 for id := WindowID(1); id <= 4; id++ {
420 sc.mon.AddWindow(id)
421 }
422 w.FocusID(4)
423 c.applyAction(ActionToggleWindowStash) // 4 stashed, shown
424 w.AddFloating(5, 20, 10)
425 settleShown(t, c)
426 var left []Placement
427 for _, p := range sc.settledLayout {
428 if p.ID == 2 || p.ID == 3 || p.ID == 4 || p.ID == 5 {
429 left = append(left, p)
430 }
431 }
432 if len(left) != 4 {
433 t.Fatalf("setup: %d placements", len(left))
434 }
435 // 2 and 3 close (absent tiles), 5 closes (absent float), 4 hides.
436 sc.mon.RemoveWindow(2)
437 sc.mon.RemoveWindow(3)
438 sc.mon.RemoveWindow(5)
439 c.applyAction(ActionToggleStashVisible)
440 for _, p := range left {
441 c.leaveFrom(sc, p, nil, ic.now)
442 }
443 var want []WindowID
444 for i := range 40 {
445 c.refreshShown()
446 var ids []WindowID
447 for _, p := range sc.settledLayout {
448 ids = append(ids, p.ID)
449 }
450 if i == 0 {
451 want = ids
452 if n := len(ids); n < 4 {
453 t.Fatalf("layout %v", ids)
454 }
455 i2, i3 := slices.Index(ids, 2), slices.Index(ids, 3)
456 i4, i5 := slices.Index(ids, 4), slices.Index(ids, 5)
457 // Absent tiles by ID, then the leaving stash float (in its
458 // slot), the absent float last.
459 if !(i2 >= 0 && i2 < i3 && i3 < i4 && i4 < i5 && i5 == len(ids)-1) {
460 t.Fatalf("order %v: want tiles 2, 3, then the stash float 4, then 5", ids)
461 }
462 if k := slices.Index(ids, 4); !sc.settledLayout[k].Leaving {
463 t.Fatal("the hidden stash window is not replaced in place")
464 }
465 continue
466 }
467 if !slices.Equal(ids, want) {
468 t.Fatalf("run %d: order %v, first run %v", i, ids, want)
469 }
470 }
471}