test(core): follow the bind order on drop, guard the drop's own spring, check every configure, animate the cross-output drop
- commit
- ecc78a71ea294e930f0200487d72594c6cbf2437
- author
- brice · · 2026-10-04 22:45 UTC
- parent
- 63a69c1
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +30 -9
M internal/core/drag.go +1 -1
| @@ -243,11 +243,11 @@ func (c *Core) endDrag(ctx context.Context) error { | |||
| 243 | 243 | now := c.now() | |
| 244 | 244 | shots := c.snapshot(now) | |
| 245 | 245 | c.drop(d.id, t) | |
| 246 | + | c.transition(shots, now) | |
| 246 | 247 | // Slots and guests follow the windows, as after a bind. | |
| 247 | 248 | if err := c.workspaceVisible(ctx, false); err != nil { | |
| 248 | 249 | return err | |
| 249 | 250 | } | |
| 250 | - | c.transition(shots, now) | |
| 251 | 251 | } | |
| 252 | 252 | } | |
| 253 | 253 | if err := c.publish(ctx); err != nil { | |
| @@ -243,11 +243,11 @@ func (c *Core) endDrag(ctx context.Context) error { | |||||
| 243 | now := c.now() | 243 | now := c.now() | ||
| 244 | shots := c.snapshot(now) | 244 | shots := c.snapshot(now) | ||
| 245 | c.drop(d.id, t) | 245 | c.drop(d.id, t) | ||
| 246 | + | c.transition(shots, now) | |||
| 246 | // Slots and guests follow the windows, as after a bind. | 247 | // Slots and guests follow the windows, as after a bind. | ||
| 247 | if err := c.workspaceVisible(ctx, false); err != nil { | 248 | if err := c.workspaceVisible(ctx, false); err != nil { | ||
| 248 | return err | 249 | return err | ||
| 249 | } | 250 | } | ||
| 250 | - | c.transition(shots, now) | |||
| 251 | } | 251 | } | ||
| 252 | } | 252 | } | ||
| 253 | if err := c.publish(ctx); err != nil { | 253 | if err := c.publish(ctx); err != nil { | ||
M internal/core/drag_test.go +29 -8
| @@ -329,6 +329,8 @@ func dragTileZonesAndHints(t *testing.T, r *dragRig) { | |||
| 329 | 329 | if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {2}, {3}}, slices.Equal) { | |
| 330 | 330 | t.Fatal(got) | |
| 331 | 331 | } | |
| 332 | + | // The drop itself must run a spring: the guard counts from here. | |
| 333 | + | r.lands = 0 | |
| 332 | 334 | r.in(ports.PointerButton{Button: 0x110}) | |
| 333 | 335 | set = r.settle(t) | |
| 334 | 336 | if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{2}, {3, 1}}, slices.Equal) { | |
| @@ -348,6 +350,8 @@ func dragTileZonesAndHints(t *testing.T, r *dragRig) { | |||
| 348 | 350 | if hints := dragScene(r.settle(t), "A").DropHints; len(hints) != 4 { | |
| 349 | 351 | t.Fatalf("swap outline %+v", hints) | |
| 350 | 352 | } | |
| 353 | + | // The drop itself must run a spring: the guard counts from here. | |
| 354 | + | r.lands = 0 | |
| 351 | 355 | r.in(ports.PointerButton{Button: 0x110}) | |
| 352 | 356 | set = r.settle(t) | |
| 353 | 357 | if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {2}, {3}}, slices.Equal) { | |
| @@ -360,6 +364,8 @@ func dragTileZonesAndHints(t *testing.T, r *dragRig) { | |||
| 360 | 364 | r.in(ports.PointerButton{Button: 0x110, Pressed: true}) | |
| 361 | 365 | r.moveTo(float64(b.X+b.W/2), float64(b.Y+b.H/2), 6) | |
| 362 | 366 | r.settle(t) | |
| 367 | + | // The drop itself must run a spring: the guard counts from here. | |
| 368 | + | r.lands = 0 | |
| 363 | 369 | r.in(ports.PointerButton{Button: 0x110}) | |
| 364 | 370 | set = r.settle(t) | |
| 365 | 371 | if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {3}, {2}}, slices.Equal) { | |
| @@ -521,15 +527,19 @@ func TestDragDropTileSlides(t *testing.T) { | |||
| 521 | 527 | if want[1].Y <= want[3].Y || want[1].X != want[3].X { | |
| 522 | 528 | t.Fatalf("1 is not stacked below 3 once settled: %+v", want) | |
| 523 | 529 | } | |
| 524 | - | // One configure for the dropped tile, to its final size. | |
| 525 | - | var sizes [][2]int | |
| 530 | + | // A window is configured at most once by the drop, to its settled | |
| 531 | + | // size, never to a size along the slide; the dropped tile always. | |
| 532 | + | sizes := map[ports.WindowID][][2]int{} | |
| 526 | 533 | for _, v := range r.drain() { | |
| 527 | - | if v, ok := v.(ports.ConfigureWindow); ok && v.ID == 1 { | |
| 528 | - | sizes = append(sizes, [2]int{v.Width, v.Height}) | |
| 534 | + | if v, ok := v.(ports.ConfigureWindow); ok { | |
| 535 | + | sizes[v.ID] = append(sizes[v.ID], [2]int{v.Width, v.Height}) | |
| 529 | 536 | } | |
| 530 | 537 | } | |
| 531 | - | if len(sizes) != 1 || sizes[0] != [2]int{want[1].W, want[1].H} { | |
| 532 | - | t.Fatalf("configures of window 1 %v, want exactly [%dx%d]", sizes, want[1].W, want[1].H) | |
| 538 | + | for id, rect := range want { | |
| 539 | + | got := sizes[id] | |
| 540 | + | if len(got) > 1 || len(got) == 1 && got[0] != [2]int{rect.W, rect.H} || id == 1 && len(got) != 1 { | |
| 541 | + | t.Fatalf("configures of window %d %v, want at most one at %dx%d (exactly one for window 1)", id, got, rect.W, rect.H) | |
| 542 | + | } | |
| 533 | 543 | } | |
| 534 | 544 | }) | |
| 535 | 545 | } | |
| @@ -557,6 +567,8 @@ func dragFixedNoGapZone(t *testing.T, r *dragRig) { | |||
| 557 | 567 | // Top band of 2 stacks 1 above it. | |
| 558 | 568 | r.moveTo(float64(two.X+two.W/2), float64(two.Y+3), 3) | |
| 559 | 569 | r.settle(t) | |
| 570 | + | // The drop itself must run a spring: the guard counts from here. | |
| 571 | + | r.lands = 0 | |
| 560 | 572 | r.in(ports.PointerButton{Button: 0x110}) | |
| 561 | 573 | if got := columnsOf(r.settle(t), "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1, 2}}, slices.Equal) { | |
| 562 | 574 | t.Fatal(got) | |
| @@ -564,8 +576,12 @@ func dragFixedNoGapZone(t *testing.T, r *dragRig) { | |||
| 564 | 576 | } | |
| 565 | 577 | ||
| 566 | 578 | func TestDragAcrossOutputs(t *testing.T) { | |
| 567 | - | b := ports.OutputInfo{Name: "B", Width: 800, Height: 600} | |
| 568 | - | r := startDragRig(t, nil, dragOut, b) | |
| 579 | + | both(t, func(t *testing.T, animated bool) { | |
| 580 | + | dragAcrossOutputs(t, startDragMode(t, animated, nil, dragOut, ports.OutputInfo{Name: "B", Width: 800, Height: 600})) | |
| 581 | + | }) | |
| 582 | + | } | |
| 583 | + | ||
| 584 | + | func dragAcrossOutputs(t *testing.T, r *dragRig) { | |
| 569 | 585 | r.client <- ports.WindowMapped{ID: 1} | |
| 570 | 586 | r.client <- ports.WindowMapped{ID: 2} | |
| 571 | 587 | r.settle(t) | |
| @@ -586,6 +602,7 @@ func TestDragAcrossOutputs(t *testing.T) { | |||
| 586 | 602 | if len(dragScene(set, "B").DropHints) == 0 || len(dragScene(set, "A").DropHints) != 0 { | |
| 587 | 603 | t.Fatalf("hints A %+v B %+v", dragScene(set, "A").DropHints, dragScene(set, "B").DropHints) | |
| 588 | 604 | } | |
| 605 | + | r.lands = 0 | |
| 589 | 606 | r.in(ports.PointerButton{Button: 0x110}) | |
| 590 | 607 | set = r.settle(t) | |
| 591 | 608 | if _, ok := windowIn(set, "B", 1); !ok { | |
| @@ -722,6 +739,8 @@ func dragFullFixedStacksInOutlinedColumn(t *testing.T, r *dragRig) { | |||
| 722 | 739 | r.press(t, 0x110, ports.ModSuper) | |
| 723 | 740 | r.moveTo(float64(one.X+one.W/2), float64(one.Y+3), 2) | |
| 724 | 741 | r.settle(t) | |
| 742 | + | // The drop itself must run a spring: the guard counts from here. | |
| 743 | + | r.lands = 0 | |
| 725 | 744 | r.in(ports.PointerButton{Button: 0x110}) | |
| 726 | 745 | set = r.settle(t) | |
| 727 | 746 | if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{4, 1}, {2}, {3}}, slices.Equal) { | |
| @@ -733,6 +752,8 @@ func dragFullFixedStacksInOutlinedColumn(t *testing.T, r *dragRig) { | |||
| 733 | 752 | r.in(ports.PointerButton{Button: 0x110, Pressed: true}) | |
| 734 | 753 | r.moveTo(float64(two.X+two.W/2), float64(two.Y+two.H/2), 4) | |
| 735 | 754 | r.settle(t) | |
| 755 | + | // The drop itself must run a spring: the guard counts from here. | |
| 756 | + | r.lands = 0 | |
| 736 | 757 | r.in(ports.PointerButton{Button: 0x110}) | |
| 737 | 758 | if got := columnsOf(r.settle(t), "A"); !slices.EqualFunc(got, [][]ports.WindowID{{4}, {2, 1}, {3}}, slices.Equal) { | |
| 738 | 759 | t.Fatal(got) | |
| @@ -329,6 +329,8 @@ func dragTileZonesAndHints(t *testing.T, r *dragRig) { | |||||
| 329 | if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {2}, {3}}, slices.Equal) { | 329 | if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {2}, {3}}, slices.Equal) { | ||
| 330 | t.Fatal(got) | 330 | t.Fatal(got) | ||
| 331 | } | 331 | } | ||
| 332 | + | // The drop itself must run a spring: the guard counts from here. | |||
| 333 | + | r.lands = 0 | |||
| 332 | r.in(ports.PointerButton{Button: 0x110}) | 334 | r.in(ports.PointerButton{Button: 0x110}) | ||
| 333 | set = r.settle(t) | 335 | set = r.settle(t) | ||
| 334 | if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{2}, {3, 1}}, slices.Equal) { | 336 | if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{2}, {3, 1}}, slices.Equal) { | ||
| @@ -348,6 +350,8 @@ func dragTileZonesAndHints(t *testing.T, r *dragRig) { | |||||
| 348 | if hints := dragScene(r.settle(t), "A").DropHints; len(hints) != 4 { | 350 | if hints := dragScene(r.settle(t), "A").DropHints; len(hints) != 4 { | ||
| 349 | t.Fatalf("swap outline %+v", hints) | 351 | t.Fatalf("swap outline %+v", hints) | ||
| 350 | } | 352 | } | ||
| 353 | + | // The drop itself must run a spring: the guard counts from here. | |||
| 354 | + | r.lands = 0 | |||
| 351 | r.in(ports.PointerButton{Button: 0x110}) | 355 | r.in(ports.PointerButton{Button: 0x110}) | ||
| 352 | set = r.settle(t) | 356 | set = r.settle(t) | ||
| 353 | if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {2}, {3}}, slices.Equal) { | 357 | if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {2}, {3}}, slices.Equal) { | ||
| @@ -360,6 +364,8 @@ func dragTileZonesAndHints(t *testing.T, r *dragRig) { | |||||
| 360 | r.in(ports.PointerButton{Button: 0x110, Pressed: true}) | 364 | r.in(ports.PointerButton{Button: 0x110, Pressed: true}) | ||
| 361 | r.moveTo(float64(b.X+b.W/2), float64(b.Y+b.H/2), 6) | 365 | r.moveTo(float64(b.X+b.W/2), float64(b.Y+b.H/2), 6) | ||
| 362 | r.settle(t) | 366 | r.settle(t) | ||
| 367 | + | // The drop itself must run a spring: the guard counts from here. | |||
| 368 | + | r.lands = 0 | |||
| 363 | r.in(ports.PointerButton{Button: 0x110}) | 369 | r.in(ports.PointerButton{Button: 0x110}) | ||
| 364 | set = r.settle(t) | 370 | set = r.settle(t) | ||
| 365 | if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {3}, {2}}, slices.Equal) { | 371 | if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1}, {3}, {2}}, slices.Equal) { | ||
| @@ -521,15 +527,19 @@ func TestDragDropTileSlides(t *testing.T) { | |||||
| 521 | if want[1].Y <= want[3].Y || want[1].X != want[3].X { | 527 | if want[1].Y <= want[3].Y || want[1].X != want[3].X { | ||
| 522 | t.Fatalf("1 is not stacked below 3 once settled: %+v", want) | 528 | t.Fatalf("1 is not stacked below 3 once settled: %+v", want) | ||
| 523 | } | 529 | } | ||
| 524 | - | // One configure for the dropped tile, to its final size. | 530 | + | // A window is configured at most once by the drop, to its settled |
| 525 | - | var sizes [][2]int | 531 | + | // size, never to a size along the slide; the dropped tile always. |
| 532 | + | sizes := map[ports.WindowID][][2]int{} | |||
| 526 | for _, v := range r.drain() { | 533 | for _, v := range r.drain() { | ||
| 527 | - | if v, ok := v.(ports.ConfigureWindow); ok && v.ID == 1 { | 534 | + | if v, ok := v.(ports.ConfigureWindow); ok { |
| 528 | - | sizes = append(sizes, [2]int{v.Width, v.Height}) | 535 | + | sizes[v.ID] = append(sizes[v.ID], [2]int{v.Width, v.Height}) |
| 529 | } | 536 | } | ||
| 530 | } | 537 | } | ||
| 531 | - | if len(sizes) != 1 || sizes[0] != [2]int{want[1].W, want[1].H} { | 538 | + | for id, rect := range want { |
| 532 | - | t.Fatalf("configures of window 1 %v, want exactly [%dx%d]", sizes, want[1].W, want[1].H) | 539 | + | got := sizes[id] |
| 540 | + | if len(got) > 1 || len(got) == 1 && got[0] != [2]int{rect.W, rect.H} || id == 1 && len(got) != 1 { | |||
| 541 | + | t.Fatalf("configures of window %d %v, want at most one at %dx%d (exactly one for window 1)", id, got, rect.W, rect.H) | |||
| 542 | + | } | |||
| 533 | } | 543 | } | ||
| 534 | }) | 544 | }) | ||
| 535 | } | 545 | } | ||
| @@ -557,6 +567,8 @@ func dragFixedNoGapZone(t *testing.T, r *dragRig) { | |||||
| 557 | // Top band of 2 stacks 1 above it. | 567 | // Top band of 2 stacks 1 above it. | ||
| 558 | r.moveTo(float64(two.X+two.W/2), float64(two.Y+3), 3) | 568 | r.moveTo(float64(two.X+two.W/2), float64(two.Y+3), 3) | ||
| 559 | r.settle(t) | 569 | r.settle(t) | ||
| 570 | + | // The drop itself must run a spring: the guard counts from here. | |||
| 571 | + | r.lands = 0 | |||
| 560 | r.in(ports.PointerButton{Button: 0x110}) | 572 | r.in(ports.PointerButton{Button: 0x110}) | ||
| 561 | if got := columnsOf(r.settle(t), "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1, 2}}, slices.Equal) { | 573 | if got := columnsOf(r.settle(t), "A"); !slices.EqualFunc(got, [][]ports.WindowID{{1, 2}}, slices.Equal) { | ||
| 562 | t.Fatal(got) | 574 | t.Fatal(got) | ||
| @@ -564,8 +576,12 @@ func dragFixedNoGapZone(t *testing.T, r *dragRig) { | |||||
| 564 | } | 576 | } | ||
| 565 | 577 | ||||
| 566 | func TestDragAcrossOutputs(t *testing.T) { | 578 | func TestDragAcrossOutputs(t *testing.T) { | ||
| 567 | - | b := ports.OutputInfo{Name: "B", Width: 800, Height: 600} | 579 | + | both(t, func(t *testing.T, animated bool) { |
| 568 | - | r := startDragRig(t, nil, dragOut, b) | 580 | + | dragAcrossOutputs(t, startDragMode(t, animated, nil, dragOut, ports.OutputInfo{Name: "B", Width: 800, Height: 600})) |
| 581 | + | }) | |||
| 582 | + | } | |||
| 583 | + | ||||
| 584 | + | func dragAcrossOutputs(t *testing.T, r *dragRig) { | |||
| 569 | r.client <- ports.WindowMapped{ID: 1} | 585 | r.client <- ports.WindowMapped{ID: 1} | ||
| 570 | r.client <- ports.WindowMapped{ID: 2} | 586 | r.client <- ports.WindowMapped{ID: 2} | ||
| 571 | r.settle(t) | 587 | r.settle(t) | ||
| @@ -586,6 +602,7 @@ func TestDragAcrossOutputs(t *testing.T) { | |||||
| 586 | if len(dragScene(set, "B").DropHints) == 0 || len(dragScene(set, "A").DropHints) != 0 { | 602 | if len(dragScene(set, "B").DropHints) == 0 || len(dragScene(set, "A").DropHints) != 0 { | ||
| 587 | t.Fatalf("hints A %+v B %+v", dragScene(set, "A").DropHints, dragScene(set, "B").DropHints) | 603 | t.Fatalf("hints A %+v B %+v", dragScene(set, "A").DropHints, dragScene(set, "B").DropHints) | ||
| 588 | } | 604 | } | ||
| 605 | + | r.lands = 0 | |||
| 589 | r.in(ports.PointerButton{Button: 0x110}) | 606 | r.in(ports.PointerButton{Button: 0x110}) | ||
| 590 | set = r.settle(t) | 607 | set = r.settle(t) | ||
| 591 | if _, ok := windowIn(set, "B", 1); !ok { | 608 | if _, ok := windowIn(set, "B", 1); !ok { | ||
| @@ -722,6 +739,8 @@ func dragFullFixedStacksInOutlinedColumn(t *testing.T, r *dragRig) { | |||||
| 722 | r.press(t, 0x110, ports.ModSuper) | 739 | r.press(t, 0x110, ports.ModSuper) | ||
| 723 | r.moveTo(float64(one.X+one.W/2), float64(one.Y+3), 2) | 740 | r.moveTo(float64(one.X+one.W/2), float64(one.Y+3), 2) | ||
| 724 | r.settle(t) | 741 | r.settle(t) | ||
| 742 | + | // The drop itself must run a spring: the guard counts from here. | |||
| 743 | + | r.lands = 0 | |||
| 725 | r.in(ports.PointerButton{Button: 0x110}) | 744 | r.in(ports.PointerButton{Button: 0x110}) | ||
| 726 | set = r.settle(t) | 745 | set = r.settle(t) | ||
| 727 | if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{4, 1}, {2}, {3}}, slices.Equal) { | 746 | if got := columnsOf(set, "A"); !slices.EqualFunc(got, [][]ports.WindowID{{4, 1}, {2}, {3}}, slices.Equal) { | ||
| @@ -733,6 +752,8 @@ func dragFullFixedStacksInOutlinedColumn(t *testing.T, r *dragRig) { | |||||
| 733 | r.in(ports.PointerButton{Button: 0x110, Pressed: true}) | 752 | r.in(ports.PointerButton{Button: 0x110, Pressed: true}) | ||
| 734 | r.moveTo(float64(two.X+two.W/2), float64(two.Y+two.H/2), 4) | 753 | r.moveTo(float64(two.X+two.W/2), float64(two.Y+two.H/2), 4) | ||
| 735 | r.settle(t) | 754 | r.settle(t) | ||
| 755 | + | // The drop itself must run a spring: the guard counts from here. | |||
| 756 | + | r.lands = 0 | |||
| 736 | r.in(ports.PointerButton{Button: 0x110}) | 757 | r.in(ports.PointerButton{Button: 0x110}) | ||
| 737 | if got := columnsOf(r.settle(t), "A"); !slices.EqualFunc(got, [][]ports.WindowID{{4}, {2, 1}, {3}}, slices.Equal) { | 758 | if got := columnsOf(r.settle(t), "A"); !slices.EqualFunc(got, [][]ports.WindowID{{4}, {2, 1}, {3}}, slices.Equal) { | ||
| 738 | t.Fatal(got) | 759 | t.Fatal(got) | ||