fix(core): a closed window leaves from where it was drawn, ordered by the two-pass rule
unmapWindow hands leaveFrom the snapshot's rectShot of the closed window, so one closed mid-motion (its entrance, a slide) exits from its drawn rect and fade with their speeds instead of jumping to its settled rect. Absent leaving tiles take withLeaving's two-pass order (C6); a tile closed while the stash hides is drawn before the leaving stash floats, the same on every publish. configures.prune forgets a destroyed window once it left the scene: no configure reaches it during or after the fade (tested).
- commit
- d7e6236ed7e8a27e7e027427065b1db38545fdb9
- author
- brice · · 2026-10-05 00:43 UTC
- parent
- df88f4a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +101 -18
M internal/core/transition.go +14 -6
| @@ -427,10 +427,12 @@ func (c *Core) appearMapped(id WindowID, now time.Time) { | |||
| 427 | 427 | ||
| 428 | 428 | // unmapWindow handles a window's unmap: it leaves its workspace, the tiles | |
| 429 | 429 | // around it re-flow with rect motions, and a window that was drawn as a | |
| 430 | - | // tile or an ordinary float keeps fading out from its last settled | |
| 431 | - | // placement (leave). Like a map, an unmap is a client event that animates; | |
| 432 | - | // the settled layout is the same with animations off. A fullscreen window, | |
| 433 | - | // one under a session lock or with the overview open just goes. | |
| 430 | + | // tile or an ordinary float keeps fading out from where it was drawn | |
| 431 | + | // (leaveFrom with the snapshot's record of it: a window closed mid-motion | |
| 432 | + | // leaves from its drawn rect, fade and speed, not from its settled rect). | |
| 433 | + | // Like a map, an unmap is a client event that animates; the settled layout | |
| 434 | + | // is the same with animations off. A fullscreen window, one under a | |
| 435 | + | // session lock or with the overview open just goes. | |
| 434 | 436 | func (c *Core) unmapWindow(v ports.WindowUnmapped) { | |
| 435 | 437 | sc, w := c.screenOf(v.ID) | |
| 436 | 438 | if sc == nil { | |
| @@ -443,7 +445,7 @@ func (c *Core) unmapWindow(v ports.WindowUnmapped) { | |||
| 443 | 445 | } | |
| 444 | 446 | now := c.now() | |
| 445 | 447 | // The placement that leaves is the settled one of the last publish: | |
| 446 | - | // where the window was going (its rect motion, if any, ends with it). | |
| 448 | + | // where the window was going. | |
| 447 | 449 | var left Placement | |
| 448 | 450 | if sc.mon.Current() == w { | |
| 449 | 451 | for _, p := range sc.settledLayout { | |
| @@ -454,16 +456,22 @@ func (c *Core) unmapWindow(v ports.WindowUnmapped) { | |||
| 454 | 456 | } | |
| 455 | 457 | } | |
| 456 | 458 | shots := c.snapshot(now) | |
| 459 | + | var shot *rectShot | |
| 457 | 460 | for i := range shots { | |
| 458 | 461 | if shots[i].overview { | |
| 459 | 462 | shots[i].ws = nil | |
| 460 | 463 | } | |
| 464 | + | if shots[i].sc == sc { | |
| 465 | + | if j := slices.IndexFunc(shots[i].rects, func(r rectShot) bool { return r.id == v.ID }); j >= 0 { | |
| 466 | + | shot = &shots[i].rects[j] | |
| 467 | + | } | |
| 468 | + | } | |
| 461 | 469 | } | |
| 462 | 470 | sc.mon.RemoveWindow(v.ID) | |
| 463 | 471 | delete(sc.rects, v.ID) | |
| 464 | 472 | c.transition(shots, now) | |
| 465 | 473 | if left.ID == v.ID && !left.Leaving && !left.Fullscreen && !left.Below && !left.Peek { | |
| 466 | - | c.leave(sc, left, now) | |
| 474 | + | c.leaveFrom(sc, left, shot, now) | |
| 467 | 475 | } | |
| 468 | 476 | } | |
| 469 | 477 | ||
| @@ -427,10 +427,12 @@ func (c *Core) appearMapped(id WindowID, now time.Time) { | |||||
| 427 | 427 | ||||
| 428 | // unmapWindow handles a window's unmap: it leaves its workspace, the tiles | 428 | // unmapWindow handles a window's unmap: it leaves its workspace, the tiles | ||
| 429 | // around it re-flow with rect motions, and a window that was drawn as a | 429 | // around it re-flow with rect motions, and a window that was drawn as a | ||
| 430 | - | // tile or an ordinary float keeps fading out from its last settled | 430 | + | // tile or an ordinary float keeps fading out from where it was drawn |
| 431 | - | // placement (leave). Like a map, an unmap is a client event that animates; | 431 | + | // (leaveFrom with the snapshot's record of it: a window closed mid-motion |
| 432 | - | // the settled layout is the same with animations off. A fullscreen window, | 432 | + | // leaves from its drawn rect, fade and speed, not from its settled rect). |
| 433 | - | // one under a session lock or with the overview open just goes. | 433 | + | // Like a map, an unmap is a client event that animates; the settled layout |
| 434 | + | // is the same with animations off. A fullscreen window, one under a | |||
| 435 | + | // session lock or with the overview open just goes. | |||
| 434 | func (c *Core) unmapWindow(v ports.WindowUnmapped) { | 436 | func (c *Core) unmapWindow(v ports.WindowUnmapped) { | ||
| 435 | sc, w := c.screenOf(v.ID) | 437 | sc, w := c.screenOf(v.ID) | ||
| 436 | if sc == nil { | 438 | if sc == nil { | ||
| @@ -443,7 +445,7 @@ func (c *Core) unmapWindow(v ports.WindowUnmapped) { | |||||
| 443 | } | 445 | } | ||
| 444 | now := c.now() | 446 | now := c.now() | ||
| 445 | // The placement that leaves is the settled one of the last publish: | 447 | // The placement that leaves is the settled one of the last publish: | ||
| 446 | - | // where the window was going (its rect motion, if any, ends with it). | 448 | + | // where the window was going. |
| 447 | var left Placement | 449 | var left Placement | ||
| 448 | if sc.mon.Current() == w { | 450 | if sc.mon.Current() == w { | ||
| 449 | for _, p := range sc.settledLayout { | 451 | for _, p := range sc.settledLayout { | ||
| @@ -454,16 +456,22 @@ func (c *Core) unmapWindow(v ports.WindowUnmapped) { | |||||
| 454 | } | 456 | } | ||
| 455 | } | 457 | } | ||
| 456 | shots := c.snapshot(now) | 458 | shots := c.snapshot(now) | ||
| 459 | + | var shot *rectShot | |||
| 457 | for i := range shots { | 460 | for i := range shots { | ||
| 458 | if shots[i].overview { | 461 | if shots[i].overview { | ||
| 459 | shots[i].ws = nil | 462 | shots[i].ws = nil | ||
| 460 | } | 463 | } | ||
| 464 | + | if shots[i].sc == sc { | |||
| 465 | + | if j := slices.IndexFunc(shots[i].rects, func(r rectShot) bool { return r.id == v.ID }); j >= 0 { | |||
| 466 | + | shot = &shots[i].rects[j] | |||
| 467 | + | } | |||
| 468 | + | } | |||
| 461 | } | 469 | } | ||
| 462 | sc.mon.RemoveWindow(v.ID) | 470 | sc.mon.RemoveWindow(v.ID) | ||
| 463 | delete(sc.rects, v.ID) | 471 | delete(sc.rects, v.ID) | ||
| 464 | c.transition(shots, now) | 472 | c.transition(shots, now) | ||
| 465 | if left.ID == v.ID && !left.Leaving && !left.Fullscreen && !left.Below && !left.Peek { | 473 | if left.ID == v.ID && !left.Leaving && !left.Fullscreen && !left.Below && !left.Peek { | ||
| 466 | - | c.leave(sc, left, now) | 474 | + | c.leaveFrom(sc, left, shot, now) |
| 467 | } | 475 | } | ||
| 468 | } | 476 | } | ||
| 469 | 477 | ||||
M internal/core/transition_test.go +6 -4
| @@ -650,7 +650,7 @@ func TestDiscreteSwipeTransition(t *testing.T) { | |||
| 650 | 650 | ||
| 651 | 651 | // A window unmapped while its rect motion runs stops moving: with | |
| 652 | 652 | // animations on it stays in the scene only as a leaving entry (drawn where | |
| 653 | - | // it was going, fading out, hidden to input), the others keep their | |
| 653 | + | // it was, fading out and shrinking, hidden to input), the others keep their | |
| 654 | 654 | // motions, and once everything settles it is gone and no frame is wanted: | |
| 655 | 655 | // a motion kept for a gone window would ask for frames with nothing to | |
| 656 | 656 | // move. With animations off it leaves the scene at once. | |
| @@ -672,15 +672,17 @@ func TestUnmapMidRectMotion(t *testing.T) { | |||
| 672 | 672 | } | |
| 673 | 673 | } | |
| 674 | 674 | moving := rectNow(t, mid, 2).X | |
| 675 | + | drawn3 := rectNow(t, mid, 3) | |
| 675 | 676 | r.client <- ports.WindowUnmapped{ID: 3} | |
| 676 | 677 | s := scene(t, r.scenes) | |
| 677 | 678 | w3, shown3 := listedIn(s, 3) | |
| 678 | 679 | switch { | |
| 679 | 680 | case tt.edit != nil && shown3: | |
| 680 | 681 | t.Fatalf("the unmapped window is still shown with animations off: %+v", w3) | |
| 681 | - | case tt.edit == nil && (!shown3 || w3.Fade != 0 || w3.Rect.X != 0 || w3.Rect.W != 400): | |
| 682 | - | // It leaves from the settled rect it was moving to. | |
| 683 | - | t.Fatalf("the unmapped window right after the unmap: %+v (shown %t), want it leaving from its settled slot", w3, shown3) | |
| 682 | + | case tt.edit == nil && (!shown3 || w3.Fade != 0 || w3.Rect != drawn3): | |
| 683 | + | // It leaves from where it was drawn, not from the settled | |
| 684 | + | // slot it was moving to. | |
| 685 | + | t.Fatalf("the unmapped window right after the unmap: %+v (shown %t), want it leaving from %+v", w3, shown3, drawn3) | |
| 684 | 686 | } | |
| 685 | 687 | // The neighbour's own motion carries on from where it was drawn. | |
| 686 | 688 | if got := rectNow(t, s, 2).X; got != moving { | |
| @@ -650,7 +650,7 @@ func TestDiscreteSwipeTransition(t *testing.T) { | |||||
| 650 | 650 | ||||
| 651 | // A window unmapped while its rect motion runs stops moving: with | 651 | // A window unmapped while its rect motion runs stops moving: with | ||
| 652 | // animations on it stays in the scene only as a leaving entry (drawn where | 652 | // animations on it stays in the scene only as a leaving entry (drawn where | ||
| 653 | - | // it was going, fading out, hidden to input), the others keep their | 653 | + | // it was, fading out and shrinking, hidden to input), the others keep their |
| 654 | // motions, and once everything settles it is gone and no frame is wanted: | 654 | // motions, and once everything settles it is gone and no frame is wanted: | ||
| 655 | // a motion kept for a gone window would ask for frames with nothing to | 655 | // a motion kept for a gone window would ask for frames with nothing to | ||
| 656 | // move. With animations off it leaves the scene at once. | 656 | // move. With animations off it leaves the scene at once. | ||
| @@ -672,15 +672,17 @@ func TestUnmapMidRectMotion(t *testing.T) { | |||||
| 672 | } | 672 | } | ||
| 673 | } | 673 | } | ||
| 674 | moving := rectNow(t, mid, 2).X | 674 | moving := rectNow(t, mid, 2).X | ||
| 675 | + | drawn3 := rectNow(t, mid, 3) | |||
| 675 | r.client <- ports.WindowUnmapped{ID: 3} | 676 | r.client <- ports.WindowUnmapped{ID: 3} | ||
| 676 | s := scene(t, r.scenes) | 677 | s := scene(t, r.scenes) | ||
| 677 | w3, shown3 := listedIn(s, 3) | 678 | w3, shown3 := listedIn(s, 3) | ||
| 678 | switch { | 679 | switch { | ||
| 679 | case tt.edit != nil && shown3: | 680 | case tt.edit != nil && shown3: | ||
| 680 | t.Fatalf("the unmapped window is still shown with animations off: %+v", w3) | 681 | t.Fatalf("the unmapped window is still shown with animations off: %+v", w3) | ||
| 681 | - | case tt.edit == nil && (!shown3 || w3.Fade != 0 || w3.Rect.X != 0 || w3.Rect.W != 400): | 682 | + | case tt.edit == nil && (!shown3 || w3.Fade != 0 || w3.Rect != drawn3): |
| 682 | - | // It leaves from the settled rect it was moving to. | 683 | + | // It leaves from where it was drawn, not from the settled |
| 683 | - | t.Fatalf("the unmapped window right after the unmap: %+v (shown %t), want it leaving from its settled slot", w3, shown3) | 684 | + | // slot it was moving to. |
| 685 | + | t.Fatalf("the unmapped window right after the unmap: %+v (shown %t), want it leaving from %+v", w3, shown3, drawn3) | |||
| 684 | } | 686 | } | ||
| 685 | // The neighbour's own motion carries on from where it was drawn. | 687 | // The neighbour's own motion carries on from where it was drawn. | ||
| 686 | if got := rectNow(t, s, 2).X; got != moving { | 688 | if got := rectNow(t, s, 2).X; got != moving { | ||
M internal/core/transition_unmap_internal_test.go +81 -8
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package core | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "math" | |
| 4 | 5 | "slices" | |
| 5 | 6 | "testing" | |
| 6 | 7 | "time" | |
| @@ -86,6 +87,14 @@ func TestUnmapTiledLeavesAndReflows(t *testing.T) { | |||
| 86 | 87 | if len(sc.rects) != 0 || c.animating() { | |
| 87 | 88 | t.Fatal("still animating after the settle") | |
| 88 | 89 | } | |
| 90 | + | // The destroyed window got no configure at any point, and its last one | |
| 91 | + | // is forgotten once it left the scene. | |
| 92 | + | if ids := drainConfigures(cmds); slices.Contains(ids, 2) { | |
| 93 | + | t.Fatalf("the destroyed window was configured: %v", ids) | |
| 94 | + | } | |
| 95 | + | if _, ok := c.configures.sent[2]; ok { | |
| 96 | + | t.Fatal("configures still remember the destroyed window") | |
| 97 | + | } | |
| 89 | 98 | } | |
| 90 | 99 | ||
| 91 | 100 | // A float leaves too, from its settled rect. | |
| @@ -112,28 +121,92 @@ func TestUnmapFloatingLeaves(t *testing.T) { | |||
| 112 | 121 | } | |
| 113 | 122 | } | |
| 114 | 123 | ||
| 115 | - | // A window closed while its own rect motion runs leaves from the rect it | |
| 116 | - | // was going to, its motion replaced by the leaving one. | |
| 117 | - | func TestUnmapMidMotionLeavesFromSettled(t *testing.T) { | |
| 124 | + | // A window closed while its own rect motion runs (its entrance here) | |
| 125 | + | // leaves from what was drawn: the first leaving frame shows the rect and | |
| 126 | + | // fade it had, not its settled rect, and the leaving motion replaces the | |
| 127 | + | // running one. | |
| 128 | + | func TestUnmapMidMotionLeavesFromDrawn(t *testing.T) { | |
| 118 | 129 | c, ic, _, sc := twoSettled(t) | |
| 119 | 130 | c.mapWindow(ports.WindowMapped{ID: 3}) | |
| 120 | 131 | indicatorScene(t, c) | |
| 121 | 132 | ic.now = ic.now.Add(30 * time.Millisecond) | |
| 122 | 133 | c.animate(ic.now, nil) | |
| 123 | - | indicatorScene(t, c) | |
| 134 | + | before := sceneWindow(t, indicatorScene(t, c), 3) | |
| 124 | 135 | settled3 := settledOf(t, sc, 3) | |
| 125 | - | if drawn := sceneWindow(t, indicatorScene(t, c), 3).Rect; drawn == settled3.Rect { | |
| 126 | - | t.Fatal("window 3 is not in flight: the case checks nothing") | |
| 136 | + | if before.Rect == settled3.Rect || !(before.Fade > 0 && before.Fade < 1) { | |
| 137 | + | t.Fatalf("window 3 is not in flight (%+v): the case checks nothing", before) | |
| 127 | 138 | } | |
| 128 | 139 | c.unmapWindow(ports.WindowUnmapped{ID: 3}) | |
| 129 | 140 | s := indicatorScene(t, c) | |
| 130 | 141 | w := sceneWindow(t, s, 3) | |
| 131 | - | if w.Rect != settled3.Rect || w.Fade != 0 { | |
| 132 | - | t.Fatalf("leaving window right after the unmap: %+v, want its settled %+v", w, settled3.Rect) | |
| 142 | + | if w.Rect != before.Rect || math.Abs(w.Fade-before.Fade) > 1e-9 { | |
| 143 | + | t.Fatalf("leaving window right after the unmap: %+v, want drawn as before %+v", w, before) | |
| 133 | 144 | } | |
| 134 | 145 | if rm, ok := sc.rects[3]; !ok || !rm.leaving { | |
| 135 | 146 | t.Fatalf("no leaving motion for window 3: %+v", rm) | |
| 136 | 147 | } | |
| 148 | + | ic.now = ic.now.Add(5 * time.Second) | |
| 149 | + | c.animate(ic.now, nil) | |
| 150 | + | if s := indicatorScene(t, c); slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool { return w.ID == 3 }) || len(sc.rects) != 0 { | |
| 151 | + | t.Fatalf("the closed window is still there: %+v", s.Windows) | |
| 152 | + | } | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | // A tile closed while the stash hides: the absent leaving tile goes by | |
| 156 | + | // withLeaving's two-pass rule (absent tiles by ID before the floats, the | |
| 157 | + | // hidden stash windows replaced in place), the same on every refresh. | |
| 158 | + | func TestUnmapWhileStashHidesOrdersDeterministically(t *testing.T) { | |
| 159 | + | c, ic, sc, cmds := stashRig(t, true) | |
| 160 | + | sc.mon.AddWindow(5) | |
| 161 | + | sc.mon.AddWindow(6) | |
| 162 | + | settleShown(t, c) | |
| 163 | + | drainConfigures(cmds) | |
| 164 | + | act(c, ic, ActionToggleStashVisible) // 2 and 3 hide, leaving in place | |
| 165 | + | c.unmapWindow(ports.WindowUnmapped{ID: 5}) | |
| 166 | + | var want []WindowID | |
| 167 | + | for i := range 20 { | |
| 168 | + | s := indicatorScene(t, c) | |
| 169 | + | var ids []WindowID | |
| 170 | + | for _, w := range s.Windows { | |
| 171 | + | ids = append(ids, w.ID) | |
| 172 | + | } | |
| 173 | + | if i == 0 { | |
| 174 | + | want = ids | |
| 175 | + | k := slices.Index(sc.settledLayout, settledOf(t, sc, 5)) | |
| 176 | + | if k < 0 || !sc.settledLayout[k].Leaving || !sc.shown[k].Leaving { | |
| 177 | + | t.Fatalf("the closed tile is not a leaving entry: %+v", sc.settledLayout) | |
| 178 | + | } | |
| 179 | + | for _, id := range []WindowID{2, 3} { | |
| 180 | + | if j := slices.Index(ids, id); j < 0 || !sc.settledLayout[j].Leaving { | |
| 181 | + | t.Fatalf("stash window %d not replaced in place: %v", id, ids) | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | // The absent tile sits before any drawn float (the leaving | |
| 185 | + | // stash windows); a hidden, undrawn one may precede it. | |
| 186 | + | i5 := slices.Index(ids, 5) | |
| 187 | + | for j, w := range s.Windows { | |
| 188 | + | if w.Floating && !w.Hidden && j < i5 { | |
| 189 | + | t.Fatalf("order %v: float %d before the leaving tile 5", ids, w.ID) | |
| 190 | + | } | |
| 191 | + | } | |
| 192 | + | continue | |
| 193 | + | } | |
| 194 | + | if !slices.Equal(ids, want) { | |
| 195 | + | t.Fatalf("publish %d: order %v, first %v", i, ids, want) | |
| 196 | + | } | |
| 197 | + | } | |
| 198 | + | if ids := drainConfigures(cmds); slices.Contains(ids, 5) { | |
| 199 | + | t.Fatalf("the closed window was configured: %v", ids) | |
| 200 | + | } | |
| 201 | + | ic.now = ic.now.Add(5 * time.Second) | |
| 202 | + | c.animate(ic.now, nil) | |
| 203 | + | s := indicatorScene(t, c) | |
| 204 | + | if slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool { return w.ID == 5 }) || len(sc.rects) != 0 { | |
| 205 | + | t.Fatalf("after the settle: %+v", s.Windows) | |
| 206 | + | } | |
| 207 | + | if ids := drainConfigures(cmds); slices.Contains(ids, 5) { | |
| 208 | + | t.Fatalf("the destroyed window was configured after the fade: %v", ids) | |
| 209 | + | } | |
| 137 | 210 | } | |
| 138 | 211 | ||
| 139 | 212 | // With animations off an unmap is instant: the window is gone, no motion. | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package core | 1 | package core | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "math" | |||
| 4 | "slices" | 5 | "slices" | ||
| 5 | "testing" | 6 | "testing" | ||
| 6 | "time" | 7 | "time" | ||
| @@ -86,6 +87,14 @@ func TestUnmapTiledLeavesAndReflows(t *testing.T) { | |||||
| 86 | if len(sc.rects) != 0 || c.animating() { | 87 | if len(sc.rects) != 0 || c.animating() { | ||
| 87 | t.Fatal("still animating after the settle") | 88 | t.Fatal("still animating after the settle") | ||
| 88 | } | 89 | } | ||
| 90 | + | // The destroyed window got no configure at any point, and its last one | |||
| 91 | + | // is forgotten once it left the scene. | |||
| 92 | + | if ids := drainConfigures(cmds); slices.Contains(ids, 2) { | |||
| 93 | + | t.Fatalf("the destroyed window was configured: %v", ids) | |||
| 94 | + | } | |||
| 95 | + | if _, ok := c.configures.sent[2]; ok { | |||
| 96 | + | t.Fatal("configures still remember the destroyed window") | |||
| 97 | + | } | |||
| 89 | } | 98 | } | ||
| 90 | 99 | ||||
| 91 | // A float leaves too, from its settled rect. | 100 | // A float leaves too, from its settled rect. | ||
| @@ -112,28 +121,92 @@ func TestUnmapFloatingLeaves(t *testing.T) { | |||||
| 112 | } | 121 | } | ||
| 113 | } | 122 | } | ||
| 114 | 123 | ||||
| 115 | - | // A window closed while its own rect motion runs leaves from the rect it | 124 | + | // A window closed while its own rect motion runs (its entrance here) |
| 116 | - | // was going to, its motion replaced by the leaving one. | 125 | + | // leaves from what was drawn: the first leaving frame shows the rect and |
| 117 | - | func TestUnmapMidMotionLeavesFromSettled(t *testing.T) { | 126 | + | // fade it had, not its settled rect, and the leaving motion replaces the |
| 127 | + | // running one. | |||
| 128 | + | func TestUnmapMidMotionLeavesFromDrawn(t *testing.T) { | |||
| 118 | c, ic, _, sc := twoSettled(t) | 129 | c, ic, _, sc := twoSettled(t) | ||
| 119 | c.mapWindow(ports.WindowMapped{ID: 3}) | 130 | c.mapWindow(ports.WindowMapped{ID: 3}) | ||
| 120 | indicatorScene(t, c) | 131 | indicatorScene(t, c) | ||
| 121 | ic.now = ic.now.Add(30 * time.Millisecond) | 132 | ic.now = ic.now.Add(30 * time.Millisecond) | ||
| 122 | c.animate(ic.now, nil) | 133 | c.animate(ic.now, nil) | ||
| 123 | - | indicatorScene(t, c) | 134 | + | before := sceneWindow(t, indicatorScene(t, c), 3) |
| 124 | settled3 := settledOf(t, sc, 3) | 135 | settled3 := settledOf(t, sc, 3) | ||
| 125 | - | if drawn := sceneWindow(t, indicatorScene(t, c), 3).Rect; drawn == settled3.Rect { | 136 | + | if before.Rect == settled3.Rect || !(before.Fade > 0 && before.Fade < 1) { |
| 126 | - | t.Fatal("window 3 is not in flight: the case checks nothing") | 137 | + | t.Fatalf("window 3 is not in flight (%+v): the case checks nothing", before) |
| 127 | } | 138 | } | ||
| 128 | c.unmapWindow(ports.WindowUnmapped{ID: 3}) | 139 | c.unmapWindow(ports.WindowUnmapped{ID: 3}) | ||
| 129 | s := indicatorScene(t, c) | 140 | s := indicatorScene(t, c) | ||
| 130 | w := sceneWindow(t, s, 3) | 141 | w := sceneWindow(t, s, 3) | ||
| 131 | - | if w.Rect != settled3.Rect || w.Fade != 0 { | 142 | + | if w.Rect != before.Rect || math.Abs(w.Fade-before.Fade) > 1e-9 { |
| 132 | - | t.Fatalf("leaving window right after the unmap: %+v, want its settled %+v", w, settled3.Rect) | 143 | + | t.Fatalf("leaving window right after the unmap: %+v, want drawn as before %+v", w, before) |
| 133 | } | 144 | } | ||
| 134 | if rm, ok := sc.rects[3]; !ok || !rm.leaving { | 145 | if rm, ok := sc.rects[3]; !ok || !rm.leaving { | ||
| 135 | t.Fatalf("no leaving motion for window 3: %+v", rm) | 146 | t.Fatalf("no leaving motion for window 3: %+v", rm) | ||
| 136 | } | 147 | } | ||
| 148 | + | ic.now = ic.now.Add(5 * time.Second) | |||
| 149 | + | c.animate(ic.now, nil) | |||
| 150 | + | if s := indicatorScene(t, c); slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool { return w.ID == 3 }) || len(sc.rects) != 0 { | |||
| 151 | + | t.Fatalf("the closed window is still there: %+v", s.Windows) | |||
| 152 | + | } | |||
| 153 | + | } | |||
| 154 | + | ||||
| 155 | + | // A tile closed while the stash hides: the absent leaving tile goes by | |||
| 156 | + | // withLeaving's two-pass rule (absent tiles by ID before the floats, the | |||
| 157 | + | // hidden stash windows replaced in place), the same on every refresh. | |||
| 158 | + | func TestUnmapWhileStashHidesOrdersDeterministically(t *testing.T) { | |||
| 159 | + | c, ic, sc, cmds := stashRig(t, true) | |||
| 160 | + | sc.mon.AddWindow(5) | |||
| 161 | + | sc.mon.AddWindow(6) | |||
| 162 | + | settleShown(t, c) | |||
| 163 | + | drainConfigures(cmds) | |||
| 164 | + | act(c, ic, ActionToggleStashVisible) // 2 and 3 hide, leaving in place | |||
| 165 | + | c.unmapWindow(ports.WindowUnmapped{ID: 5}) | |||
| 166 | + | var want []WindowID | |||
| 167 | + | for i := range 20 { | |||
| 168 | + | s := indicatorScene(t, c) | |||
| 169 | + | var ids []WindowID | |||
| 170 | + | for _, w := range s.Windows { | |||
| 171 | + | ids = append(ids, w.ID) | |||
| 172 | + | } | |||
| 173 | + | if i == 0 { | |||
| 174 | + | want = ids | |||
| 175 | + | k := slices.Index(sc.settledLayout, settledOf(t, sc, 5)) | |||
| 176 | + | if k < 0 || !sc.settledLayout[k].Leaving || !sc.shown[k].Leaving { | |||
| 177 | + | t.Fatalf("the closed tile is not a leaving entry: %+v", sc.settledLayout) | |||
| 178 | + | } | |||
| 179 | + | for _, id := range []WindowID{2, 3} { | |||
| 180 | + | if j := slices.Index(ids, id); j < 0 || !sc.settledLayout[j].Leaving { | |||
| 181 | + | t.Fatalf("stash window %d not replaced in place: %v", id, ids) | |||
| 182 | + | } | |||
| 183 | + | } | |||
| 184 | + | // The absent tile sits before any drawn float (the leaving | |||
| 185 | + | // stash windows); a hidden, undrawn one may precede it. | |||
| 186 | + | i5 := slices.Index(ids, 5) | |||
| 187 | + | for j, w := range s.Windows { | |||
| 188 | + | if w.Floating && !w.Hidden && j < i5 { | |||
| 189 | + | t.Fatalf("order %v: float %d before the leaving tile 5", ids, w.ID) | |||
| 190 | + | } | |||
| 191 | + | } | |||
| 192 | + | continue | |||
| 193 | + | } | |||
| 194 | + | if !slices.Equal(ids, want) { | |||
| 195 | + | t.Fatalf("publish %d: order %v, first %v", i, ids, want) | |||
| 196 | + | } | |||
| 197 | + | } | |||
| 198 | + | if ids := drainConfigures(cmds); slices.Contains(ids, 5) { | |||
| 199 | + | t.Fatalf("the closed window was configured: %v", ids) | |||
| 200 | + | } | |||
| 201 | + | ic.now = ic.now.Add(5 * time.Second) | |||
| 202 | + | c.animate(ic.now, nil) | |||
| 203 | + | s := indicatorScene(t, c) | |||
| 204 | + | if slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool { return w.ID == 5 }) || len(sc.rects) != 0 { | |||
| 205 | + | t.Fatalf("after the settle: %+v", s.Windows) | |||
| 206 | + | } | |||
| 207 | + | if ids := drainConfigures(cmds); slices.Contains(ids, 5) { | |||
| 208 | + | t.Fatalf("the destroyed window was configured after the fade: %v", ids) | |||
| 209 | + | } | |||
| 137 | } | 210 | } | ||
| 138 | 211 | ||||
| 139 | // With animations off an unmap is instant: the window is gone, no motion. | 212 | // With animations off an unmap is instant: the window is gone, no motion. | ||