fix(core): keep the destination of a moved workspace off the switch slide, cover named pulls and mixed geometry
- commit
- 937984f7b81c8e1f5c3c1e97ebd633f5e18c4379
- author
- brice · · 2026-10-04 22:53 UTC
- parent
- d27f5ec
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +137 -8
M internal/core/transition.go +14 -8
| @@ -407,11 +407,13 @@ func (c *Core) snapshot(now time.Time) []viewShot { | |||
| 407 | 407 | // off, with the overview open before or after, or while a swipe follows the | |
| 408 | 408 | // fingers on that screen. Per-window rect motions join the camera here. | |
| 409 | 409 | // | |
| 410 | - | // A workspace moved to another monitor (move-workspace-to-monitor-*) is | |
| 411 | - | // animated on both screens: the destination's windows slide in from where | |
| 412 | - | // the source drew them, the source slides to its next workspace. The | |
| 413 | - | // focus-monitor-* actions move nothing: no window changes place, so they | |
| 414 | - | // are not animated (the focus pulse marks the new focus). | |
| 410 | + | // A workspace moved to another monitor (move-workspace-to-monitor-*, or a | |
| 411 | + | // named workspace pulled to the focused one by a "workspace <name>" bind | |
| 412 | + | // while it shows on another) is animated on both screens: the destination's | |
| 413 | + | // windows slide in from where the source drew them (no workspace slide | |
| 414 | + | // there), the source slides to its next workspace. The focus-monitor-* | |
| 415 | + | // actions move nothing: no window changes place, so they are not animated | |
| 416 | + | // (the focus pulse marks the new focus). | |
| 415 | 417 | func (c *Core) transition(before []viewShot, now time.Time) { | |
| 416 | 418 | if !c.animOn() { | |
| 417 | 419 | return | |
| @@ -421,7 +423,7 @@ func (c *Core) transition(before []viewShot, now time.Time) { | |||
| 421 | 423 | if b.ws == nil || !c.hasScreen(b.sc) || b.sc.mon.ov.open || c.following(b.sc) { | |
| 422 | 424 | continue | |
| 423 | 425 | } | |
| 424 | - | c.transitionCamera(b, now) | |
| 426 | + | c.transitionCamera(b, before, now) | |
| 425 | 427 | c.transitionRects(b, before, now) | |
| 426 | 428 | } | |
| 427 | 429 | } | |
| @@ -433,7 +435,7 @@ func (c *Core) following(sc *screen) bool { | |||
| 433 | 435 | } | |
| 434 | 436 | ||
| 435 | 437 | // transitionCamera slides the columns or the monitor from what b showed. | |
| 436 | - | func (c *Core) transitionCamera(b *viewShot, now time.Time) { | |
| 438 | + | func (c *Core) transitionCamera(b *viewShot, before []viewShot, now time.Time) { | |
| 437 | 439 | m := b.sc.mon | |
| 438 | 440 | w := m.Current() | |
| 439 | 441 | switch { | |
| @@ -445,7 +447,11 @@ func (c *Core) transitionCamera(b *viewShot, now time.Time) { | |||
| 445 | 447 | // carries over, and the spring starts now like every action's. | |
| 446 | 448 | w.shift = b.view - float64(w.ViewX) | |
| 447 | 449 | w.motion = c.spring(viewSpring(w.shift, b.viewV), now) | |
| 448 | - | case b.ok && m.shown == nil: | |
| 450 | + | case b.ok && m.shown == nil && movedFrom(b, before) == nil: | |
| 451 | + | // A screen that received its workspace from another monitor does | |
| 452 | + | // not slide to it: a running slide's list (switchList) may still | |
| 453 | + | // hold the workspace, which would start a slide from nowhere. Only | |
| 454 | + | // its windows move (transitionRects). | |
| 449 | 455 | j := indexOf(b.list, w) | |
| 450 | 456 | if j < 0 { | |
| 451 | 457 | return | |
| @@ -407,11 +407,13 @@ func (c *Core) snapshot(now time.Time) []viewShot { | |||||
| 407 | // off, with the overview open before or after, or while a swipe follows the | 407 | // off, with the overview open before or after, or while a swipe follows the | ||
| 408 | // fingers on that screen. Per-window rect motions join the camera here. | 408 | // fingers on that screen. Per-window rect motions join the camera here. | ||
| 409 | // | 409 | // | ||
| 410 | - | // A workspace moved to another monitor (move-workspace-to-monitor-*) is | 410 | + | // A workspace moved to another monitor (move-workspace-to-monitor-*, or a |
| 411 | - | // animated on both screens: the destination's windows slide in from where | 411 | + | // named workspace pulled to the focused one by a "workspace <name>" bind |
| 412 | - | // the source drew them, the source slides to its next workspace. The | 412 | + | // while it shows on another) is animated on both screens: the destination's |
| 413 | - | // focus-monitor-* actions move nothing: no window changes place, so they | 413 | + | // windows slide in from where the source drew them (no workspace slide |
| 414 | - | // are not animated (the focus pulse marks the new focus). | 414 | + | // there), the source slides to its next workspace. The focus-monitor-* |
| 415 | + | // actions move nothing: no window changes place, so they are not animated | |||
| 416 | + | // (the focus pulse marks the new focus). | |||
| 415 | func (c *Core) transition(before []viewShot, now time.Time) { | 417 | func (c *Core) transition(before []viewShot, now time.Time) { | ||
| 416 | if !c.animOn() { | 418 | if !c.animOn() { | ||
| 417 | return | 419 | return | ||
| @@ -421,7 +423,7 @@ func (c *Core) transition(before []viewShot, now time.Time) { | |||||
| 421 | if b.ws == nil || !c.hasScreen(b.sc) || b.sc.mon.ov.open || c.following(b.sc) { | 423 | if b.ws == nil || !c.hasScreen(b.sc) || b.sc.mon.ov.open || c.following(b.sc) { | ||
| 422 | continue | 424 | continue | ||
| 423 | } | 425 | } | ||
| 424 | - | c.transitionCamera(b, now) | 426 | + | c.transitionCamera(b, before, now) |
| 425 | c.transitionRects(b, before, now) | 427 | c.transitionRects(b, before, now) | ||
| 426 | } | 428 | } | ||
| 427 | } | 429 | } | ||
| @@ -433,7 +435,7 @@ func (c *Core) following(sc *screen) bool { | |||||
| 433 | } | 435 | } | ||
| 434 | 436 | ||||
| 435 | // transitionCamera slides the columns or the monitor from what b showed. | 437 | // transitionCamera slides the columns or the monitor from what b showed. | ||
| 436 | - | func (c *Core) transitionCamera(b *viewShot, now time.Time) { | 438 | + | func (c *Core) transitionCamera(b *viewShot, before []viewShot, now time.Time) { |
| 437 | m := b.sc.mon | 439 | m := b.sc.mon | ||
| 438 | w := m.Current() | 440 | w := m.Current() | ||
| 439 | switch { | 441 | switch { | ||
| @@ -445,7 +447,11 @@ func (c *Core) transitionCamera(b *viewShot, now time.Time) { | |||||
| 445 | // carries over, and the spring starts now like every action's. | 447 | // carries over, and the spring starts now like every action's. | ||
| 446 | w.shift = b.view - float64(w.ViewX) | 448 | w.shift = b.view - float64(w.ViewX) | ||
| 447 | w.motion = c.spring(viewSpring(w.shift, b.viewV), now) | 449 | w.motion = c.spring(viewSpring(w.shift, b.viewV), now) | ||
| 448 | - | case b.ok && m.shown == nil: | 450 | + | case b.ok && m.shown == nil && movedFrom(b, before) == nil: |
| 451 | + | // A screen that received its workspace from another monitor does | |||
| 452 | + | // not slide to it: a running slide's list (switchList) may still | |||
| 453 | + | // hold the workspace, which would start a slide from nowhere. Only | |||
| 454 | + | // its windows move (transitionRects). | |||
| 449 | j := indexOf(b.list, w) | 455 | j := indexOf(b.list, w) | ||
| 450 | if j < 0 { | 456 | if j < 0 { | ||
| 451 | return | 457 | return | ||
M internal/core/transition_test.go +123 -0
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "testing" | |
| 5 | 5 | "time" | |
| 6 | 6 | ||
| 7 | + | "github.com/bnema/neferwl/internal/core" | |
| 7 | 8 | "github.com/bnema/neferwl/internal/ports" | |
| 8 | 9 | ) | |
| 9 | 10 | ||
| @@ -263,6 +264,128 @@ func TestMovedWorkspaceOffIsInstant(t *testing.T) { | |||
| 263 | 264 | r.noFrameScene(t, wide.Name, other.Name) | |
| 264 | 265 | } | |
| 265 | 266 | ||
| 267 | + | // A workspace that comes back to the screen it left within the spring of | |
| 268 | + | // its first move slides its windows only: the screen's running workspace | |
| 269 | + | // slide (its list still holds the workspace) must not start a vertical one. | |
| 270 | + | func TestMovedBackWorkspaceHasNoSwitchSlide(t *testing.T) { | |
| 271 | + | r, other := movedRig(t, nil) | |
| 272 | + | twoColumns(t, r) | |
| 273 | + | r.key(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift) | |
| 274 | + | // Within the first spring: the source is still sliding. | |
| 275 | + | if _, ok := r.clk.flip(t, r.frames, r.scenes, 16*time.Millisecond, wide.Name, other.Name); !ok { | |
| 276 | + | t.Fatal("no frame while the windows slide") | |
| 277 | + | } | |
| 278 | + | set := r.key(t, "Left", ports.ModAlt|ports.ModCtrl|ports.ModShift) | |
| 279 | + | dst := outScene(t, set, wide.Name) | |
| 280 | + | for id := ports.WindowID(1); id <= 2; id++ { | |
| 281 | + | if got := rectNow(t, dst, id); got.Y != 0 { | |
| 282 | + | t.Fatalf("window %d at %+v right after moving back, want y=0 (no vertical slide)", id, got) | |
| 283 | + | } | |
| 284 | + | } | |
| 285 | + | set, ok := r.clk.flip(t, r.frames, r.scenes, 16*time.Millisecond, wide.Name, other.Name) | |
| 286 | + | if !ok { | |
| 287 | + | t.Fatal("no frame while the windows slide back") | |
| 288 | + | } | |
| 289 | + | dst = outScene(t, set, wide.Name) | |
| 290 | + | for id := ports.WindowID(1); id <= 2; id++ { | |
| 291 | + | if got := rectNow(t, dst, id); got.Y != 0 { | |
| 292 | + | t.Fatalf("window %d at %+v on the first frame, want y=0 (rects slide only)", id, got) | |
| 293 | + | } | |
| 294 | + | } | |
| 295 | + | set, _ = r.settleAll(t, wide.Name, other.Name) | |
| 296 | + | dst = outScene(t, set, wide.Name) | |
| 297 | + | if got1, got2 := rectNow(t, dst, 1), rectNow(t, dst, 2); got1 != (ports.Rect{W: 400, H: 600}) || got2 != (ports.Rect{X: 400, W: 400, H: 600}) { | |
| 298 | + | t.Fatalf("settled 1 at %+v and 2 at %+v", got1, got2) | |
| 299 | + | } | |
| 300 | + | r.noFrameScene(t, wide.Name, other.Name) | |
| 301 | + | } | |
| 302 | + | ||
| 303 | + | // The start rects are the source's translated by the difference of the | |
| 304 | + | // outputs' positions on both axes, with the source's size; they settle on | |
| 305 | + | // the destination's layout. | |
| 306 | + | func TestMovedWorkspaceStartsFromSourceGeometry(t *testing.T) { | |
| 307 | + | big := ports.OutputInfo{Name: "DP-2", Width: 1024, Height: 768, RefreshMilli: 60000} | |
| 308 | + | for _, tc := range []struct { | |
| 309 | + | name string | |
| 310 | + | dst ports.OutputInfo | |
| 311 | + | anchor ports.OutputAnchor | |
| 312 | + | key string | |
| 313 | + | start, final [2]ports.Rect | |
| 314 | + | }{ | |
| 315 | + | { | |
| 316 | + | name: "right same size", dst: ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000}, | |
| 317 | + | anchor: ports.OutputAnchor{Relation: ports.RelationRightOf, To: "DP-1"}, key: "Right", | |
| 318 | + | start: [2]ports.Rect{{X: -800, W: 400, H: 600}, {X: -400, W: 400, H: 600}}, | |
| 319 | + | final: [2]ports.Rect{{W: 400, H: 600}, {X: 400, W: 400, H: 600}}, | |
| 320 | + | }, | |
| 321 | + | { | |
| 322 | + | name: "below larger", dst: big, | |
| 323 | + | anchor: ports.OutputAnchor{Relation: ports.RelationBelow, To: "DP-1"}, key: "Down", | |
| 324 | + | start: [2]ports.Rect{{Y: -600, W: 400, H: 600}, {X: 400, Y: -600, W: 400, H: 600}}, | |
| 325 | + | final: [2]ports.Rect{{W: 512, H: 768}, {X: 512, W: 512, H: 768}}, | |
| 326 | + | }, | |
| 327 | + | } { | |
| 328 | + | t.Run(tc.name, func(t *testing.T) { | |
| 329 | + | r := startSwipeOn(t, func(c *ports.Config) { | |
| 330 | + | c.Outputs = []ports.OutputConfig{{Name: "DP-1"}, {Name: "DP-2", Anchor: tc.anchor}} | |
| 331 | + | c.Binds["Cmd+Ctrl+Shift+Down"] = string(core.ActionMoveWorkspaceToMonitorDown) | |
| 332 | + | }, wide, tc.dst) | |
| 333 | + | twoColumns(t, r) | |
| 334 | + | dst := outScene(t, r.key(t, tc.key, ports.ModAlt|ports.ModCtrl|ports.ModShift), tc.dst.Name) | |
| 335 | + | for i, id := range []ports.WindowID{1, 2} { | |
| 336 | + | if got := rectNow(t, dst, id); got != tc.start[i] { | |
| 337 | + | t.Fatalf("window %d starts at %+v, want %+v", id, got, tc.start[i]) | |
| 338 | + | } | |
| 339 | + | } | |
| 340 | + | set, ok := r.clk.flip(t, r.frames, r.scenes, 16*time.Millisecond, wide.Name, tc.dst.Name) | |
| 341 | + | if !ok { | |
| 342 | + | t.Fatal("no frame while the windows slide") | |
| 343 | + | } | |
| 344 | + | if got := rectNow(t, outScene(t, set, tc.dst.Name), 1); got == tc.start[0] || got == tc.final[0] { | |
| 345 | + | t.Fatalf("window 1 at %+v on the first frame, want it between start and final", got) | |
| 346 | + | } | |
| 347 | + | set, _ = r.settleAll(t, wide.Name, tc.dst.Name) | |
| 348 | + | dst = outScene(t, set, tc.dst.Name) | |
| 349 | + | for i, id := range []ports.WindowID{1, 2} { | |
| 350 | + | if got := rectNow(t, dst, id); got != tc.final[i] { | |
| 351 | + | t.Fatalf("window %d settles at %+v, want %+v", id, got, tc.final[i]) | |
| 352 | + | } | |
| 353 | + | } | |
| 354 | + | }) | |
| 355 | + | } | |
| 356 | + | } | |
| 357 | + | ||
| 358 | + | // A named workspace pulled to the focused screen from the one showing it | |
| 359 | + | // moves like a monitor move: its windows slide in from the source. | |
| 360 | + | func TestPulledNamedWorkspaceSlidesIn(t *testing.T) { | |
| 361 | + | r, other := movedRig(t, func(c *ports.Config) { | |
| 362 | + | c.Workspaces = []ports.WorkspaceConfig{{Name: "web"}} | |
| 363 | + | c.Binds["Alt+w"] = "workspace web" | |
| 364 | + | }) | |
| 365 | + | // web shows on DP-2 with a window, then DP-1 pulls it. | |
| 366 | + | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |
| 367 | + | r.key(t, "w", ports.ModAlt) | |
| 368 | + | r.settleAll(t, wide.Name, other.Name) | |
| 369 | + | r.mapWindow(t, 1) | |
| 370 | + | r.settleAll(t, wide.Name, other.Name) | |
| 371 | + | r.key(t, "Left", ports.ModAlt|ports.ModCtrl) | |
| 372 | + | set := r.key(t, "w", ports.ModAlt) | |
| 373 | + | if got := rectNow(t, outScene(t, set, wide.Name), 1); got.X != 800 { | |
| 374 | + | t.Fatalf("window 1 at %+v right after the pull, want x=800 (where DP-2 drew it)", got) | |
| 375 | + | } | |
| 376 | + | set, ok := r.clk.flip(t, r.frames, r.scenes, 16*time.Millisecond, wide.Name, other.Name) | |
| 377 | + | if !ok { | |
| 378 | + | t.Fatal("no frame while the window slides") | |
| 379 | + | } | |
| 380 | + | if got := rectNow(t, outScene(t, set, wide.Name), 1).X; got <= 0 || got >= 800 { | |
| 381 | + | t.Fatalf("window 1 at x=%d on the first frame, want between 0 and 800", got) | |
| 382 | + | } | |
| 383 | + | set, _ = r.settleAll(t, wide.Name, other.Name) | |
| 384 | + | if got := rectNow(t, outScene(t, set, wide.Name), 1); got != (ports.Rect{W: 800, H: 600}) { | |
| 385 | + | t.Fatalf("settled at %+v", got) | |
| 386 | + | } | |
| 387 | + | } | |
| 388 | + | ||
| 266 | 389 | // Focusing another monitor moves no window: it is not animated. | |
| 267 | 390 | func TestFocusMonitorHasNoMotion(t *testing.T) { | |
| 268 | 391 | r, other := movedRig(t, nil) | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "testing" | 4 | "testing" | ||
| 5 | "time" | 5 | "time" | ||
| 6 | 6 | ||||
| 7 | + | "github.com/bnema/neferwl/internal/core" | |||
| 7 | "github.com/bnema/neferwl/internal/ports" | 8 | "github.com/bnema/neferwl/internal/ports" | ||
| 8 | ) | 9 | ) | ||
| 9 | 10 | ||||
| @@ -263,6 +264,128 @@ func TestMovedWorkspaceOffIsInstant(t *testing.T) { | |||||
| 263 | r.noFrameScene(t, wide.Name, other.Name) | 264 | r.noFrameScene(t, wide.Name, other.Name) | ||
| 264 | } | 265 | } | ||
| 265 | 266 | ||||
| 267 | + | // A workspace that comes back to the screen it left within the spring of | |||
| 268 | + | // its first move slides its windows only: the screen's running workspace | |||
| 269 | + | // slide (its list still holds the workspace) must not start a vertical one. | |||
| 270 | + | func TestMovedBackWorkspaceHasNoSwitchSlide(t *testing.T) { | |||
| 271 | + | r, other := movedRig(t, nil) | |||
| 272 | + | twoColumns(t, r) | |||
| 273 | + | r.key(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift) | |||
| 274 | + | // Within the first spring: the source is still sliding. | |||
| 275 | + | if _, ok := r.clk.flip(t, r.frames, r.scenes, 16*time.Millisecond, wide.Name, other.Name); !ok { | |||
| 276 | + | t.Fatal("no frame while the windows slide") | |||
| 277 | + | } | |||
| 278 | + | set := r.key(t, "Left", ports.ModAlt|ports.ModCtrl|ports.ModShift) | |||
| 279 | + | dst := outScene(t, set, wide.Name) | |||
| 280 | + | for id := ports.WindowID(1); id <= 2; id++ { | |||
| 281 | + | if got := rectNow(t, dst, id); got.Y != 0 { | |||
| 282 | + | t.Fatalf("window %d at %+v right after moving back, want y=0 (no vertical slide)", id, got) | |||
| 283 | + | } | |||
| 284 | + | } | |||
| 285 | + | set, ok := r.clk.flip(t, r.frames, r.scenes, 16*time.Millisecond, wide.Name, other.Name) | |||
| 286 | + | if !ok { | |||
| 287 | + | t.Fatal("no frame while the windows slide back") | |||
| 288 | + | } | |||
| 289 | + | dst = outScene(t, set, wide.Name) | |||
| 290 | + | for id := ports.WindowID(1); id <= 2; id++ { | |||
| 291 | + | if got := rectNow(t, dst, id); got.Y != 0 { | |||
| 292 | + | t.Fatalf("window %d at %+v on the first frame, want y=0 (rects slide only)", id, got) | |||
| 293 | + | } | |||
| 294 | + | } | |||
| 295 | + | set, _ = r.settleAll(t, wide.Name, other.Name) | |||
| 296 | + | dst = outScene(t, set, wide.Name) | |||
| 297 | + | if got1, got2 := rectNow(t, dst, 1), rectNow(t, dst, 2); got1 != (ports.Rect{W: 400, H: 600}) || got2 != (ports.Rect{X: 400, W: 400, H: 600}) { | |||
| 298 | + | t.Fatalf("settled 1 at %+v and 2 at %+v", got1, got2) | |||
| 299 | + | } | |||
| 300 | + | r.noFrameScene(t, wide.Name, other.Name) | |||
| 301 | + | } | |||
| 302 | + | ||||
| 303 | + | // The start rects are the source's translated by the difference of the | |||
| 304 | + | // outputs' positions on both axes, with the source's size; they settle on | |||
| 305 | + | // the destination's layout. | |||
| 306 | + | func TestMovedWorkspaceStartsFromSourceGeometry(t *testing.T) { | |||
| 307 | + | big := ports.OutputInfo{Name: "DP-2", Width: 1024, Height: 768, RefreshMilli: 60000} | |||
| 308 | + | for _, tc := range []struct { | |||
| 309 | + | name string | |||
| 310 | + | dst ports.OutputInfo | |||
| 311 | + | anchor ports.OutputAnchor | |||
| 312 | + | key string | |||
| 313 | + | start, final [2]ports.Rect | |||
| 314 | + | }{ | |||
| 315 | + | { | |||
| 316 | + | name: "right same size", dst: ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000}, | |||
| 317 | + | anchor: ports.OutputAnchor{Relation: ports.RelationRightOf, To: "DP-1"}, key: "Right", | |||
| 318 | + | start: [2]ports.Rect{{X: -800, W: 400, H: 600}, {X: -400, W: 400, H: 600}}, | |||
| 319 | + | final: [2]ports.Rect{{W: 400, H: 600}, {X: 400, W: 400, H: 600}}, | |||
| 320 | + | }, | |||
| 321 | + | { | |||
| 322 | + | name: "below larger", dst: big, | |||
| 323 | + | anchor: ports.OutputAnchor{Relation: ports.RelationBelow, To: "DP-1"}, key: "Down", | |||
| 324 | + | start: [2]ports.Rect{{Y: -600, W: 400, H: 600}, {X: 400, Y: -600, W: 400, H: 600}}, | |||
| 325 | + | final: [2]ports.Rect{{W: 512, H: 768}, {X: 512, W: 512, H: 768}}, | |||
| 326 | + | }, | |||
| 327 | + | } { | |||
| 328 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 329 | + | r := startSwipeOn(t, func(c *ports.Config) { | |||
| 330 | + | c.Outputs = []ports.OutputConfig{{Name: "DP-1"}, {Name: "DP-2", Anchor: tc.anchor}} | |||
| 331 | + | c.Binds["Cmd+Ctrl+Shift+Down"] = string(core.ActionMoveWorkspaceToMonitorDown) | |||
| 332 | + | }, wide, tc.dst) | |||
| 333 | + | twoColumns(t, r) | |||
| 334 | + | dst := outScene(t, r.key(t, tc.key, ports.ModAlt|ports.ModCtrl|ports.ModShift), tc.dst.Name) | |||
| 335 | + | for i, id := range []ports.WindowID{1, 2} { | |||
| 336 | + | if got := rectNow(t, dst, id); got != tc.start[i] { | |||
| 337 | + | t.Fatalf("window %d starts at %+v, want %+v", id, got, tc.start[i]) | |||
| 338 | + | } | |||
| 339 | + | } | |||
| 340 | + | set, ok := r.clk.flip(t, r.frames, r.scenes, 16*time.Millisecond, wide.Name, tc.dst.Name) | |||
| 341 | + | if !ok { | |||
| 342 | + | t.Fatal("no frame while the windows slide") | |||
| 343 | + | } | |||
| 344 | + | if got := rectNow(t, outScene(t, set, tc.dst.Name), 1); got == tc.start[0] || got == tc.final[0] { | |||
| 345 | + | t.Fatalf("window 1 at %+v on the first frame, want it between start and final", got) | |||
| 346 | + | } | |||
| 347 | + | set, _ = r.settleAll(t, wide.Name, tc.dst.Name) | |||
| 348 | + | dst = outScene(t, set, tc.dst.Name) | |||
| 349 | + | for i, id := range []ports.WindowID{1, 2} { | |||
| 350 | + | if got := rectNow(t, dst, id); got != tc.final[i] { | |||
| 351 | + | t.Fatalf("window %d settles at %+v, want %+v", id, got, tc.final[i]) | |||
| 352 | + | } | |||
| 353 | + | } | |||
| 354 | + | }) | |||
| 355 | + | } | |||
| 356 | + | } | |||
| 357 | + | ||||
| 358 | + | // A named workspace pulled to the focused screen from the one showing it | |||
| 359 | + | // moves like a monitor move: its windows slide in from the source. | |||
| 360 | + | func TestPulledNamedWorkspaceSlidesIn(t *testing.T) { | |||
| 361 | + | r, other := movedRig(t, func(c *ports.Config) { | |||
| 362 | + | c.Workspaces = []ports.WorkspaceConfig{{Name: "web"}} | |||
| 363 | + | c.Binds["Alt+w"] = "workspace web" | |||
| 364 | + | }) | |||
| 365 | + | // web shows on DP-2 with a window, then DP-1 pulls it. | |||
| 366 | + | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |||
| 367 | + | r.key(t, "w", ports.ModAlt) | |||
| 368 | + | r.settleAll(t, wide.Name, other.Name) | |||
| 369 | + | r.mapWindow(t, 1) | |||
| 370 | + | r.settleAll(t, wide.Name, other.Name) | |||
| 371 | + | r.key(t, "Left", ports.ModAlt|ports.ModCtrl) | |||
| 372 | + | set := r.key(t, "w", ports.ModAlt) | |||
| 373 | + | if got := rectNow(t, outScene(t, set, wide.Name), 1); got.X != 800 { | |||
| 374 | + | t.Fatalf("window 1 at %+v right after the pull, want x=800 (where DP-2 drew it)", got) | |||
| 375 | + | } | |||
| 376 | + | set, ok := r.clk.flip(t, r.frames, r.scenes, 16*time.Millisecond, wide.Name, other.Name) | |||
| 377 | + | if !ok { | |||
| 378 | + | t.Fatal("no frame while the window slides") | |||
| 379 | + | } | |||
| 380 | + | if got := rectNow(t, outScene(t, set, wide.Name), 1).X; got <= 0 || got >= 800 { | |||
| 381 | + | t.Fatalf("window 1 at x=%d on the first frame, want between 0 and 800", got) | |||
| 382 | + | } | |||
| 383 | + | set, _ = r.settleAll(t, wide.Name, other.Name) | |||
| 384 | + | if got := rectNow(t, outScene(t, set, wide.Name), 1); got != (ports.Rect{W: 800, H: 600}) { | |||
| 385 | + | t.Fatalf("settled at %+v", got) | |||
| 386 | + | } | |||
| 387 | + | } | |||
| 388 | + | ||||
| 266 | // Focusing another monitor moves no window: it is not animated. | 389 | // Focusing another monitor moves no window: it is not animated. | ||
| 267 | func TestFocusMonitorHasNoMotion(t *testing.T) { | 390 | func TestFocusMonitorHasNoMotion(t *testing.T) { | ||
| 268 | r, other := movedRig(t, nil) | 391 | r, other := movedRig(t, nil) | ||