1package core
2
3import (
4 "math"
5 "slices"
6 "testing"
7 "time"
8
9 "github.com/bnema/neferwl/internal/ports"
10)
11
12// A tiled map: the new window fades in from 90 % of its settled rect, its
13// neighbour slides from where it was, the client is configured once, to
14// the final size, and everything settles exactly.
15func TestMapTiledAppearsAndReflows(t *testing.T) {
16 c, ic, cmds := fadeCore(t)
17 c.cfg.Animations.On = true
18 sc := c.cur()
19 c.mapWindow(ports.WindowMapped{ID: 1})
20 indicatorScene(t, c)
21 c.animate(ic.now.Add(5*time.Second), nil)
22 ic.now = ic.now.Add(5 * time.Second)
23 indicatorScene(t, c)
24 if len(sc.rects) != 0 {
25 t.Fatalf("motions left after the first window settled: %d", len(sc.rects))
26 }
27 drainConfigures(cmds)
28 old1 := sceneWindow(t, indicatorScene(t, c), 1).Rect
29
30 c.mapWindow(ports.WindowMapped{ID: 2})
31 s := indicatorScene(t, c)
32 settled2 := settledOf(t, sc, 2)
33 w2 := sceneWindow(t, s, 2)
34 if w2.Fade != 1 || w2.Hidden || w2.Rect != scaledRect(settled2.Rect, appearScale) || math.Abs(w2.Zoom-appearScale) > 0.02 || w2.Preview != 0 {
35 t.Fatalf("new window right after the map: %+v, settled %+v", w2, settled2.Rect)
36 }
37 // The neighbour is drawn where it was, and moves to its new half.
38 if got := sceneWindow(t, s, 1).Rect; got != old1 {
39 t.Fatalf("neighbour right after the map at %+v, want the old %+v", got, old1)
40 }
41 settled1 := settledOf(t, sc, 1)
42 if settled1.Rect == old1 {
43 t.Fatal("the new column did not move the neighbour: the case checks nothing")
44 }
45 // One configure per window, to the settled size.
46 got := drainConfigures(cmds)
47 slices.Sort(got)
48 if !slices.Equal(got, []WindowID{1, 2}) {
49 t.Fatalf("configures %v, want one each for 1 and 2", got)
50 }
51
52 ic.now = ic.now.Add(40 * time.Millisecond)
53 c.animate(ic.now, nil)
54 s = indicatorScene(t, c)
55 w2 = sceneWindow(t, s, 2)
56 if !(w2.Fade > 0 && w2.Fade < 1) || !(w2.Zoom > appearScale && w2.Zoom < 1) || !(w2.Rect.W > scaledRect(settled2.Rect, appearScale).W && w2.Rect.W < settled2.Rect.W) {
57 t.Fatalf("first frame of the new window: %+v", w2)
58 }
59 if r := sceneWindow(t, s, 1).Rect; !(r.W < old1.W && r.W > settled1.Rect.W) {
60 t.Fatalf("neighbour on the first frame %+v, want between %+v and %+v", r, old1, settled1.Rect)
61 }
62 if ids := drainConfigures(cmds); len(ids) != 0 {
63 t.Fatalf("frames reconfigured %v", ids)
64 }
65
66 ic.now = ic.now.Add(5 * time.Second)
67 c.animate(ic.now, nil)
68 s = indicatorScene(t, c)
69 if w := sceneWindow(t, s, 2); w.Fade != 0 || w.Zoom != 0 || w.Rect != settled2.Rect {
70 t.Fatalf("new window settled: %+v, want %+v", w, settled2.Rect)
71 }
72 if w := sceneWindow(t, s, 1); w.Rect != settled1.Rect {
73 t.Fatalf("neighbour settled at %+v, want %+v", w.Rect, settled1.Rect)
74 }
75 if len(sc.rects) != 0 || c.animating() {
76 t.Fatal("still animating after the settle")
77 }
78}
79
80// A floating toplevel appears too.
81func TestMapFloatingAppears(t *testing.T) {
82 c, ic, _ := fadeCore(t)
83 c.cfg.Animations.On = true
84 sc := c.cur()
85 c.mapWindow(ports.WindowMapped{ID: 1})
86 indicatorScene(t, c)
87 c.mapWindow(ports.WindowMapped{ID: 2, Floating: true, Width: 80, Height: 60})
88 s := indicatorScene(t, c)
89 settled := settledOf(t, sc, 2)
90 w := sceneWindow(t, s, 2)
91 if !w.Floating && !settled.Floating {
92 t.Fatalf("window 2 is not a float: %+v", settled)
93 }
94 if w.Fade != 1 || w.Rect != scaledRect(settled.Rect, appearScale) {
95 t.Fatalf("float right after the map: %+v, settled %+v", w, settled.Rect)
96 }
97 ic.now = ic.now.Add(5 * time.Second)
98 c.animate(ic.now, nil)
99 if w := sceneWindow(t, indicatorScene(t, c), 2); w.Fade != 0 || w.Rect != settled.Rect {
100 t.Fatalf("float settled: %+v", w)
101 }
102}
103
104// With animations off a map is instant: no motion, no fade.
105func TestMapOffIsInstant(t *testing.T) {
106 c, _, _ := fadeCore(t)
107 sc := c.cur()
108 c.mapWindow(ports.WindowMapped{ID: 1})
109 indicatorScene(t, c)
110 c.mapWindow(ports.WindowMapped{ID: 2})
111 s := indicatorScene(t, c)
112 if len(sc.rects) != 0 || c.animating() {
113 t.Fatal("a map started a motion with animations off")
114 }
115 for _, w := range s.Windows {
116 if w.Fade != 0 || w.Zoom != 0 || w.Rect != settledOf(t, sc, w.ID).Rect {
117 t.Fatalf("window %d not at its settled rect: %+v", w.ID, w)
118 }
119 }
120}
121
122// A window that is not shown tiled or floating on screen does not appear:
123// fullscreen, hidden behind a fullscreen game, with a session lock or with
124// the overview open. Neighbours stay put as well in the last two.
125func TestMapExceptions(t *testing.T) {
126 t.Run("fullscreen", func(t *testing.T) {
127 c, ic, _ := fadeCore(t)
128 c.cfg.Animations.On = true
129 sc := c.cur()
130 c.mapWindow(ports.WindowMapped{ID: 1})
131 c.stopAnimations() // the first window's own entrance
132 sc.mon.SetFullscreen(1, true)
133 c.appearMapped(1, ic.now)
134 indicatorScene(t, c)
135 if len(sc.rects) != 0 || c.animating() {
136 t.Fatalf("a fullscreen window appeared: %+v", sc.rects)
137 }
138 })
139 t.Run("behind a fullscreen game", func(t *testing.T) {
140 c, _, _ := fadeCore(t)
141 c.cfg.Animations.On = true
142 sc := c.cur()
143 sc.mon.Current().Overflow = OverflowFixed
144 c.mapWindow(ports.WindowMapped{ID: 1})
145 c.stopAnimations() // the first window's own entrance
146 sc.mon.SetFullscreen(1, true)
147 indicatorScene(t, c)
148 c.mapWindow(ports.WindowMapped{ID: 2})
149 if s := indicatorScene(t, c); len(sc.rects) != 0 || sceneWindow(t, s, 2).Fade != 0 {
150 t.Fatalf("a window mapped behind a fullscreen one appeared: %+v", sc.rects)
151 }
152 })
153 t.Run("session lock", func(t *testing.T) {
154 c, _, _ := fadeCore(t)
155 c.cfg.Animations.On = true
156 sc := c.cur()
157 c.mapWindow(ports.WindowMapped{ID: 1})
158 c.stopAnimations() // the first window's own entrance
159 c.security = ports.SecurityState{Generation: 1, Protected: true}
160 c.mapWindow(ports.WindowMapped{ID: 2})
161 if len(sc.rects) != 0 || c.animating() {
162 t.Fatalf("a window mapped under a session lock animated: %+v", sc.rects)
163 }
164 })
165 t.Run("overview open", func(t *testing.T) {
166 c, _, _ := fadeCore(t)
167 c.cfg.Animations.On = true
168 sc := c.cur()
169 c.mapWindow(ports.WindowMapped{ID: 1})
170 c.stopAnimations() // the first window's own entrance
171 sc.mon.ToggleOverview()
172 c.mapWindow(ports.WindowMapped{ID: 2})
173 if len(sc.rects) != 0 {
174 t.Fatalf("a window mapped in the overview animated: %+v", sc.rects)
175 }
176 })
177 t.Run("map again", func(t *testing.T) {
178 c, ic, _ := fadeCore(t)
179 c.cfg.Animations.On = true
180 sc := c.cur()
181 c.mapWindow(ports.WindowMapped{ID: 1})
182 c.stopAnimations() // the first window's own entrance
183 indicatorScene(t, c)
184 ic.now = ic.now.Add(5 * time.Second)
185 c.mapWindow(ports.WindowMapped{ID: 1})
186 c.stopAnimations() // the first window's own entrance
187 if len(sc.rects) != 0 {
188 t.Fatalf("a repeated map restarted the entrance: %+v", sc.rects)
189 }
190 })
191}
192
193// A float maps before its first commit: Width and Height are 0, so core lays
194// it out at a default size, and the real one comes in WindowResized. The
195// entrance must restart from 90 % of the real size, not stay distorted.
196func TestMapFloatEntranceFollowsItsFirstCommit(t *testing.T) {
197 c, ic, _ := fadeCore(t)
198 c.cfg.Animations.On = true
199 sc := c.cur()
200 c.mapWindow(ports.WindowMapped{ID: 1})
201 c.stopAnimations()
202 c.mapWindow(ports.WindowMapped{ID: 2, Floating: true})
203 indicatorScene(t, c)
204 guess := settledOf(t, sc, 2).Rect
205
206 c.resizeFloating(ports.WindowResized{ID: 2, Width: 60, Height: 20})
207 s := indicatorScene(t, c)
208 real := settledOf(t, sc, 2).Rect
209 if real == guess || real.W == real.H*guess.W/guess.H {
210 t.Fatalf("the resize did not change the settled shape: %+v then %+v", guess, real)
211 }
212 w := sceneWindow(t, s, 2)
213 want := scaledRect(real, appearScale)
214 near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }
215 if w.Fade != 1 || !near(w.Rect.X, want.X) || !near(w.Rect.Y, want.Y) || !near(w.Rect.W, want.W) || !near(w.Rect.H, want.H) {
216 t.Fatalf("first frame %+v, want 90 %% of the real %+v = %+v", w, real, want)
217 }
218 if math.Abs(w.Zoom-appearScale) > 0.02 {
219 t.Fatalf("zoom %v, want ~0.9 (aspect kept)", w.Zoom)
220 }
221
222 // Mid-way the frame keeps the real aspect, the zoom follows its width.
223 ic.now = ic.now.Add(40 * time.Millisecond)
224 c.animate(ic.now, nil)
225 w = sceneWindow(t, indicatorScene(t, c), 2)
226 ratio, wantRatio := float64(w.Rect.W)/float64(w.Rect.H), float64(real.W)/float64(real.H)
227 if math.Abs(ratio-wantRatio)/wantRatio > 0.06 || !(w.Rect.W > want.W && w.Rect.W < real.W) || math.Abs(w.Zoom-float64(w.Rect.W)/float64(real.W)) > 0.01 {
228 t.Fatalf("mid-way %+v: aspect %.3f want %.3f", w, ratio, wantRatio)
229 }
230
231 ic.now = ic.now.Add(5 * time.Second)
232 c.animate(ic.now, nil)
233 if w := sceneWindow(t, indicatorScene(t, c), 2); w.Fade != 0 || w.Zoom != 0 || w.Rect != real {
234 t.Fatalf("settled %+v, want %+v", w, real)
235 }
236}
237
238// A resize with no entrance running only moves the settled rect.
239func TestResizeFloatWithoutEntranceKeepsNoMotion(t *testing.T) {
240 c, _, _ := fadeCore(t)
241 c.cfg.Animations.On = true
242 sc := c.cur()
243 c.mapWindow(ports.WindowMapped{ID: 1, Floating: true})
244 c.stopAnimations()
245 c.resizeFloating(ports.WindowResized{ID: 1, Width: 60, Height: 20})
246 if len(sc.rects) != 0 {
247 t.Fatalf("a resize started a motion: %+v", sc.rects)
248 }
249}