fix(core): clamp the pointer after outputs change
- commit
- ca6770ed6349afa33667643dadba6e16c32331dc
- author
- brice · · 2026-10-03 19:15 UTC
- parent
- 2ee8a71
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +43 -0
M internal/core/core.go +3 -0
| @@ -1184,6 +1184,7 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 1184 | 1184 | if c.layerChanged { | |
| 1185 | 1185 | c.setLayers(c.allLayers()) | |
| 1186 | 1186 | } | |
| 1187 | + | c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY) | |
| 1187 | 1188 | case ports.OutputRemoved: | |
| 1188 | 1189 | // A drop target may be on the output going away. | |
| 1189 | 1190 | c.abortDrag() | |
| @@ -1214,6 +1215,8 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 1214 | 1215 | } | |
| 1215 | 1216 | continue | |
| 1216 | 1217 | } | |
| 1218 | + | // A rotation can shrink the layout under a still pointer. | |
| 1219 | + | c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY) | |
| 1217 | 1220 | if c.workspaceVisible(ctx, false) != nil { | |
| 1218 | 1221 | return nil | |
| 1219 | 1222 | } | |
| @@ -1184,6 +1184,7 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 1184 | if c.layerChanged { | 1184 | if c.layerChanged { | ||
| 1185 | c.setLayers(c.allLayers()) | 1185 | c.setLayers(c.allLayers()) | ||
| 1186 | } | 1186 | } | ||
| 1187 | + | c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY) | |||
| 1187 | case ports.OutputRemoved: | 1188 | case ports.OutputRemoved: | ||
| 1188 | // A drop target may be on the output going away. | 1189 | // A drop target may be on the output going away. | ||
| 1189 | c.abortDrag() | 1190 | c.abortDrag() | ||
| @@ -1214,6 +1215,8 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 1214 | } | 1215 | } | ||
| 1215 | continue | 1216 | continue | ||
| 1216 | } | 1217 | } | ||
| 1218 | + | // A rotation can shrink the layout under a still pointer. | |||
| 1219 | + | c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, c.cursorX, c.cursorY) | |||
| 1217 | if c.workspaceVisible(ctx, false) != nil { | 1220 | if c.workspaceVisible(ctx, false) != nil { | ||
| 1218 | return nil | 1221 | return nil | ||
| 1219 | } | 1222 | } | ||
M internal/core/screens_test.go +40 -0
| @@ -1426,6 +1426,9 @@ func TestRotatedOutputLayout(t *testing.T) { | |||
| 1426 | 1426 | } | |
| 1427 | 1427 | r.mapWindow(t, 1) | |
| 1428 | 1428 | set := r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |
| 1429 | + | if len(set) != 2 || set[0].Output != "DP-1" || set[1].Output != "DP-2" { | |
| 1430 | + | t.Fatalf("scenes: %+v", set) | |
| 1431 | + | } | |
| 1429 | 1432 | for _, s := range set { | |
| 1430 | 1433 | switch s.Output { | |
| 1431 | 1434 | case "DP-1": | |
| @@ -1503,3 +1506,40 @@ func TestTransformReload(t *testing.T) { | |||
| 1503 | 1506 | t.Fatalf("DP-1: %+v", o) | |
| 1504 | 1507 | } | |
| 1505 | 1508 | } | |
| 1509 | + | ||
| 1510 | + | // A reload that shrinks the layout under a still pointer brings it back onto | |
| 1511 | + | // an output. | |
| 1512 | + | func TestTransformReloadClampsPointer(t *testing.T) { | |
| 1513 | + | r := startMulti(t, func(c *ports.Config) { | |
| 1514 | + | c.Outputs = []ports.OutputConfig{{Name: "DP-1"}, {Name: "DP-2"}} | |
| 1515 | + | }, left, right) | |
| 1516 | + | r.mapWindow(t, 1) | |
| 1517 | + | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |
| 1518 | + | r.mapWindow(t, 2) | |
| 1519 | + | // DP-2 spans x 200..600: (550, 50) is (350, 50) on window 2. | |
| 1520 | + | for len(r.commands) > 0 { | |
| 1521 | + | <-r.commands | |
| 1522 | + | } | |
| 1523 | + | r.input <- ports.PointerMotion{X: 550, Y: 50} | |
| 1524 | + | if v := command(t, r.commands); v != (ports.PointerFocus{ID: 2, X: 350, Y: 50}) { | |
| 1525 | + | t.Fatal(v) | |
| 1526 | + | } | |
| 1527 | + | // Rotating DP-1 moves DP-2 to x 100..500: 550 is off every output. | |
| 1528 | + | r.cfg.Outputs[0].Transform = 1 | |
| 1529 | + | r.reload <- ports.ConfigChanged{Config: r.cfg} | |
| 1530 | + | receive(t, r.scenes) | |
| 1531 | + | for len(r.commands) > 0 { | |
| 1532 | + | <-r.commands | |
| 1533 | + | } | |
| 1534 | + | // A rehit (here from a swipe end) points at what lies under the pointer: | |
| 1535 | + | // clamped onto DP-1's right edge, window 1, not nothing. | |
| 1536 | + | r.input <- ports.SwipeEnd{} | |
| 1537 | + | for { | |
| 1538 | + | if v, ok := command(t, r.commands).(ports.PointerFocus); ok { | |
| 1539 | + | if v.ID != 1 { | |
| 1540 | + | t.Fatalf("pointer after reload: %+v", v) | |
| 1541 | + | } | |
| 1542 | + | break | |
| 1543 | + | } | |
| 1544 | + | } | |
| 1545 | + | } | |
| @@ -1426,6 +1426,9 @@ func TestRotatedOutputLayout(t *testing.T) { | |||||
| 1426 | } | 1426 | } | ||
| 1427 | r.mapWindow(t, 1) | 1427 | r.mapWindow(t, 1) | ||
| 1428 | set := r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | 1428 | set := r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | ||
| 1429 | + | if len(set) != 2 || set[0].Output != "DP-1" || set[1].Output != "DP-2" { | |||
| 1430 | + | t.Fatalf("scenes: %+v", set) | |||
| 1431 | + | } | |||
| 1429 | for _, s := range set { | 1432 | for _, s := range set { | ||
| 1430 | switch s.Output { | 1433 | switch s.Output { | ||
| 1431 | case "DP-1": | 1434 | case "DP-1": | ||
| @@ -1503,3 +1506,40 @@ func TestTransformReload(t *testing.T) { | |||||
| 1503 | t.Fatalf("DP-1: %+v", o) | 1506 | t.Fatalf("DP-1: %+v", o) | ||
| 1504 | } | 1507 | } | ||
| 1505 | } | 1508 | } | ||
| 1509 | + | ||||
| 1510 | + | // A reload that shrinks the layout under a still pointer brings it back onto | |||
| 1511 | + | // an output. | |||
| 1512 | + | func TestTransformReloadClampsPointer(t *testing.T) { | |||
| 1513 | + | r := startMulti(t, func(c *ports.Config) { | |||
| 1514 | + | c.Outputs = []ports.OutputConfig{{Name: "DP-1"}, {Name: "DP-2"}} | |||
| 1515 | + | }, left, right) | |||
| 1516 | + | r.mapWindow(t, 1) | |||
| 1517 | + | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |||
| 1518 | + | r.mapWindow(t, 2) | |||
| 1519 | + | // DP-2 spans x 200..600: (550, 50) is (350, 50) on window 2. | |||
| 1520 | + | for len(r.commands) > 0 { | |||
| 1521 | + | <-r.commands | |||
| 1522 | + | } | |||
| 1523 | + | r.input <- ports.PointerMotion{X: 550, Y: 50} | |||
| 1524 | + | if v := command(t, r.commands); v != (ports.PointerFocus{ID: 2, X: 350, Y: 50}) { | |||
| 1525 | + | t.Fatal(v) | |||
| 1526 | + | } | |||
| 1527 | + | // Rotating DP-1 moves DP-2 to x 100..500: 550 is off every output. | |||
| 1528 | + | r.cfg.Outputs[0].Transform = 1 | |||
| 1529 | + | r.reload <- ports.ConfigChanged{Config: r.cfg} | |||
| 1530 | + | receive(t, r.scenes) | |||
| 1531 | + | for len(r.commands) > 0 { | |||
| 1532 | + | <-r.commands | |||
| 1533 | + | } | |||
| 1534 | + | // A rehit (here from a swipe end) points at what lies under the pointer: | |||
| 1535 | + | // clamped onto DP-1's right edge, window 1, not nothing. | |||
| 1536 | + | r.input <- ports.SwipeEnd{} | |||
| 1537 | + | for { | |||
| 1538 | + | if v, ok := command(t, r.commands).(ports.PointerFocus); ok { | |||
| 1539 | + | if v.ID != 1 { | |||
| 1540 | + | t.Fatalf("pointer after reload: %+v", v) | |||
| 1541 | + | } | |||
| 1542 | + | break | |||
| 1543 | + | } | |||
| 1544 | + | } | |||
| 1545 | + | } | |||