fix(core): reroute data drags on layout changes and keep swipes and switcher Escape out
- commit
- 45d51dacf5e9febfed748eec432f16b28f5e3776
- author
- brice · · 2026-10-17 15:01 UTC
- parent
- a976009
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +118 -12
M internal/core/core.go +6 -2
| @@ -102,7 +102,7 @@ type Core struct { | |||
| 102 | 102 | // held, cmdMod the one keyboard.cmd names. lastButton is the latest | |
| 103 | 103 | // press a client saw, the one xdg_toplevel.move's serial names. | |
| 104 | 104 | drag *dragState | |
| 105 | - | // dataDrag is the client data drag in progress (datadrag.go); | |
| 105 | + | // dataDrag is the client data drag in progress (datadrag.go). | |
| 106 | 106 | dataDrag *dataDragState | |
| 107 | 107 | swallow map[uint32]bool | |
| 108 | 108 | lastButton uint32 | |
| @@ -762,7 +762,11 @@ func (c *Core) slidFrame(ctx context.Context, shown bool, only *screen) error { | |||
| 762 | 762 | // rehit points the pointer at what now lies under a still cursor, as | |
| 763 | 763 | // windows slide under it. A held button keeps its grab. | |
| 764 | 764 | func (c *Core) rehit(ctx context.Context) error { | |
| 765 | - | if c.grab != 0 || c.drag != nil || c.dataDrag != nil { | |
| 765 | + | if c.dataDrag != nil { | |
| 766 | + | // The drag target follows the layout too. | |
| 767 | + | return c.dataDragRetarget(ctx, c.motionTime) | |
| 768 | + | } | |
| 769 | + | if c.grab != 0 || c.drag != nil { | |
| 766 | 770 | return nil | |
| 767 | 771 | } | |
| 768 | 772 | id, x, y := c.hit(c.cursorX, c.cursorY) | |
| @@ -102,7 +102,7 @@ type Core struct { | |||||
| 102 | // held, cmdMod the one keyboard.cmd names. lastButton is the latest | 102 | // held, cmdMod the one keyboard.cmd names. lastButton is the latest | ||
| 103 | // press a client saw, the one xdg_toplevel.move's serial names. | 103 | // press a client saw, the one xdg_toplevel.move's serial names. | ||
| 104 | drag *dragState | 104 | drag *dragState | ||
| 105 | - | // dataDrag is the client data drag in progress (datadrag.go); | 105 | + | // dataDrag is the client data drag in progress (datadrag.go). |
| 106 | dataDrag *dataDragState | 106 | dataDrag *dataDragState | ||
| 107 | swallow map[uint32]bool | 107 | swallow map[uint32]bool | ||
| 108 | lastButton uint32 | 108 | lastButton uint32 | ||
| @@ -762,7 +762,11 @@ func (c *Core) slidFrame(ctx context.Context, shown bool, only *screen) error { | |||||
| 762 | // rehit points the pointer at what now lies under a still cursor, as | 762 | // rehit points the pointer at what now lies under a still cursor, as | ||
| 763 | // windows slide under it. A held button keeps its grab. | 763 | // windows slide under it. A held button keeps its grab. | ||
| 764 | func (c *Core) rehit(ctx context.Context) error { | 764 | func (c *Core) rehit(ctx context.Context) error { | ||
| 765 | - | if c.grab != 0 || c.drag != nil || c.dataDrag != nil { | 765 | + | if c.dataDrag != nil { |
| 766 | + | // The drag target follows the layout too. | |||
| 767 | + | return c.dataDragRetarget(ctx, c.motionTime) | |||
| 768 | + | } | |||
| 769 | + | if c.grab != 0 || c.drag != nil { | |||
| 766 | return nil | 770 | return nil | ||
| 767 | } | 771 | } | ||
| 768 | id, x, y := c.hit(c.cursorX, c.cursorY) | 772 | id, x, y := c.hit(c.cursorX, c.cursorY) | ||
M internal/core/datadrag.go +24 -3
| @@ -17,13 +17,15 @@ import ( | |||
| 17 | 17 | // target, told to wayland as DataDragFocus and DataDragMotion; | |
| 18 | 18 | // - releasing the drag button drops (DataDragDrop), Escape cancels | |
| 19 | 19 | // (DataDragCancel); neither reaches a client; | |
| 20 | - | // - scrolling and touchpad gestures go nowhere; the keyboard keeps its | |
| 20 | + | // - scrolling and touchpad gestures go nowhere, compositor swipes | |
| 21 | + | // included; binds still run, Escape cancels the drag before the | |
| 22 | + | // switcher; the keyboard keeps its | |
| 21 | 23 | // focus, clients read the modifiers that choose the action from it; | |
| 22 | 24 | // - the drag icon, a client surface wayland measures (DataDragIcon), is | |
| 23 | 25 | // drawn at the cursor over everything. | |
| 24 | 26 | // | |
| 25 | - | // Layout changes under a still cursor, like an unmapped target, are | |
| 26 | - | // resolved at the next motion. | |
| 27 | + | // Layout changes under a still cursor reroute the drag (rehit), and the | |
| 28 | + | // drop re-hits. | |
| 27 | 29 | ||
| 28 | 30 | // dataDragState is the data drag in progress. | |
| 29 | 31 | type dataDragState struct { | |
| @@ -57,6 +59,11 @@ func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error | |||
| 57 | 59 | } | |
| 58 | 60 | } | |
| 59 | 61 | c.grab = 0 | |
| 62 | + | // A compositor swipe in progress lets go of what it moved. | |
| 63 | + | if c.swipe != nil { | |
| 64 | + | c.dropSwipe() | |
| 65 | + | c.swipe = nil | |
| 66 | + | } | |
| 60 | 67 | // The origin gets its pointer leave, as the drag takes the pointer. | |
| 61 | 68 | if c.pointer != 0 { | |
| 62 | 69 | c.pointer = 0 | |
| @@ -84,6 +91,15 @@ func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error { | |||
| 84 | 91 | return nil | |
| 85 | 92 | } | |
| 86 | 93 | ||
| 94 | + | // dataDragRetarget re-routes a still cursor: only a changed target is | |
| 95 | + | // told. | |
| 96 | + | func (c *Core) dataDragRetarget(ctx context.Context, at time.Duration) error { | |
| 97 | + | if id, _, _ := c.hit(c.cursorX, c.cursorY); id == c.dataDrag.target { | |
| 98 | + | return nil | |
| 99 | + | } | |
| 100 | + | return c.dataDragRoute(ctx, at) | |
| 101 | + | } | |
| 102 | + | ||
| 87 | 103 | // dataDragMotion follows the cursor: the target gets the motion and the | |
| 88 | 104 | // icon moves with it. | |
| 89 | 105 | func (c *Core) dataDragMotion(ctx context.Context, at time.Duration) error { | |
| @@ -105,6 +121,11 @@ func (c *Core) dataDragButton(ctx context.Context, v ports.PointerButton) error | |||
| 105 | 121 | return nil | |
| 106 | 122 | } | |
| 107 | 123 | delete(c.buttons, v.Button) | |
| 124 | + | // A bind may have moved the layout under a still cursor: the | |
| 125 | + | // drop goes to what lies there now. | |
| 126 | + | if err := c.dataDragRetarget(ctx, v.Time); err != nil { | |
| 127 | + | return err | |
| 128 | + | } | |
| 108 | 129 | c.dataDrag = nil | |
| 109 | 130 | if err := c.command(ctx, ports.DataDragDrop{}); err != nil { | |
| 110 | 131 | return err | |
| @@ -17,13 +17,15 @@ import ( | |||||
| 17 | // target, told to wayland as DataDragFocus and DataDragMotion; | 17 | // target, told to wayland as DataDragFocus and DataDragMotion; | ||
| 18 | // - releasing the drag button drops (DataDragDrop), Escape cancels | 18 | // - releasing the drag button drops (DataDragDrop), Escape cancels | ||
| 19 | // (DataDragCancel); neither reaches a client; | 19 | // (DataDragCancel); neither reaches a client; | ||
| 20 | - | // - scrolling and touchpad gestures go nowhere; the keyboard keeps its | 20 | + | // - scrolling and touchpad gestures go nowhere, compositor swipes |
| 21 | + | // included; binds still run, Escape cancels the drag before the | |||
| 22 | + | // switcher; the keyboard keeps its | |||
| 21 | // focus, clients read the modifiers that choose the action from it; | 23 | // focus, clients read the modifiers that choose the action from it; | ||
| 22 | // - the drag icon, a client surface wayland measures (DataDragIcon), is | 24 | // - the drag icon, a client surface wayland measures (DataDragIcon), is | ||
| 23 | // drawn at the cursor over everything. | 25 | // drawn at the cursor over everything. | ||
| 24 | // | 26 | // | ||
| 25 | - | // Layout changes under a still cursor, like an unmapped target, are | 27 | + | // Layout changes under a still cursor reroute the drag (rehit), and the |
| 26 | - | // resolved at the next motion. | 28 | + | // drop re-hits. |
| 27 | 29 | ||||
| 28 | // dataDragState is the data drag in progress. | 30 | // dataDragState is the data drag in progress. | ||
| 29 | type dataDragState struct { | 31 | type dataDragState struct { | ||
| @@ -57,6 +59,11 @@ func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error | |||||
| 57 | } | 59 | } | ||
| 58 | } | 60 | } | ||
| 59 | c.grab = 0 | 61 | c.grab = 0 | ||
| 62 | + | // A compositor swipe in progress lets go of what it moved. | |||
| 63 | + | if c.swipe != nil { | |||
| 64 | + | c.dropSwipe() | |||
| 65 | + | c.swipe = nil | |||
| 66 | + | } | |||
| 60 | // The origin gets its pointer leave, as the drag takes the pointer. | 67 | // The origin gets its pointer leave, as the drag takes the pointer. | ||
| 61 | if c.pointer != 0 { | 68 | if c.pointer != 0 { | ||
| 62 | c.pointer = 0 | 69 | c.pointer = 0 | ||
| @@ -84,6 +91,15 @@ func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error { | |||||
| 84 | return nil | 91 | return nil | ||
| 85 | } | 92 | } | ||
| 86 | 93 | ||||
| 94 | + | // dataDragRetarget re-routes a still cursor: only a changed target is | |||
| 95 | + | // told. | |||
| 96 | + | func (c *Core) dataDragRetarget(ctx context.Context, at time.Duration) error { | |||
| 97 | + | if id, _, _ := c.hit(c.cursorX, c.cursorY); id == c.dataDrag.target { | |||
| 98 | + | return nil | |||
| 99 | + | } | |||
| 100 | + | return c.dataDragRoute(ctx, at) | |||
| 101 | + | } | |||
| 102 | + | ||||
| 87 | // dataDragMotion follows the cursor: the target gets the motion and the | 103 | // dataDragMotion follows the cursor: the target gets the motion and the | ||
| 88 | // icon moves with it. | 104 | // icon moves with it. | ||
| 89 | func (c *Core) dataDragMotion(ctx context.Context, at time.Duration) error { | 105 | func (c *Core) dataDragMotion(ctx context.Context, at time.Duration) error { | ||
| @@ -105,6 +121,11 @@ func (c *Core) dataDragButton(ctx context.Context, v ports.PointerButton) error | |||||
| 105 | return nil | 121 | return nil | ||
| 106 | } | 122 | } | ||
| 107 | delete(c.buttons, v.Button) | 123 | delete(c.buttons, v.Button) | ||
| 124 | + | // A bind may have moved the layout under a still cursor: the | |||
| 125 | + | // drop goes to what lies there now. | |||
| 126 | + | if err := c.dataDragRetarget(ctx, v.Time); err != nil { | |||
| 127 | + | return err | |||
| 128 | + | } | |||
| 108 | c.dataDrag = nil | 129 | c.dataDrag = nil | ||
| 109 | if err := c.command(ctx, ports.DataDragDrop{}); err != nil { | 130 | if err := c.command(ctx, ports.DataDragDrop{}); err != nil { | ||
| 110 | return err | 131 | return err | ||
M internal/core/datadrag_test.go +69 -0
| @@ -463,3 +463,72 @@ func TestDataDragFocusScreenFollowsPointer(t *testing.T) { | |||
| 463 | 463 | t.Fatalf("focus %+v", focus) | |
| 464 | 464 | } | |
| 465 | 465 | } | |
| 466 | + | ||
| 467 | + | func TestDataDragFollowsWorkspaceSwitchUnderStillCursor(t *testing.T) { | |
| 468 | + | d := startDnd(t) | |
| 469 | + | d.start(t, 0) | |
| 470 | + | d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3) | |
| 471 | + | d.settle(t) | |
| 472 | + | d.drain() | |
| 473 | + | // cmd+2 shows an empty workspace under the still cursor: the target | |
| 474 | + | // leaves, and the release drops on nothing. | |
| 475 | + | d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper, Pressed: true}) | |
| 476 | + | d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper}) | |
| 477 | + | d.settle(t) | |
| 478 | + | got := dataDragCommands(d.drain()) | |
| 479 | + | if !slices.Contains(got, ports.ClientCommand(ports.DataDragFocus{Time: 3 * time.Millisecond})) { | |
| 480 | + | t.Fatalf("no leave after the switch: %#v", got) | |
| 481 | + | } | |
| 482 | + | d.in(ports.PointerButton{Button: 0x110, Time: 4 * time.Millisecond}) | |
| 483 | + | d.settle(t) | |
| 484 | + | got = dataDragCommands(d.drain()) | |
| 485 | + | if len(got) == 0 || got[0] != (ports.DataDragDrop{}) { | |
| 486 | + | t.Fatalf("commands %#v", got) | |
| 487 | + | } | |
| 488 | + | for _, v := range got { | |
| 489 | + | if f, ok := v.(ports.DataDragFocus); ok && f.ID != 0 { | |
| 490 | + | t.Fatalf("hidden window targeted: %#v", got) | |
| 491 | + | } | |
| 492 | + | } | |
| 493 | + | } | |
| 494 | + | ||
| 495 | + | func TestDataDragEscapeCancelsOverSwitcher(t *testing.T) { | |
| 496 | + | d := startDnd(t) | |
| 497 | + | d.start(t, 0) | |
| 498 | + | d.in(ports.KeyEvent{Keysym: "Super_L", Keycode: 125, Mods: ports.ModSuper, Pressed: true}) | |
| 499 | + | d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper, Pressed: true}) | |
| 500 | + | d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper}) | |
| 501 | + | d.settle(t) | |
| 502 | + | d.drain() | |
| 503 | + | d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1, Mods: ports.ModSuper, Pressed: true}) | |
| 504 | + | d.settle(t) | |
| 505 | + | got := dataDragCommands(d.drain()) | |
| 506 | + | if len(got) == 0 || got[0] != (ports.DataDragCancel{}) { | |
| 507 | + | t.Fatalf("Escape did not cancel the drag: %#v", got) | |
| 508 | + | } | |
| 509 | + | d.in(ports.PointerButton{Button: 0x110}) | |
| 510 | + | d.settle(t) | |
| 511 | + | if slices.Contains(dataDragCommands(d.drain()), ports.ClientCommand(ports.DataDragDrop{})) { | |
| 512 | + | t.Fatal("dropped after Escape") | |
| 513 | + | } | |
| 514 | + | } | |
| 515 | + | ||
| 516 | + | func TestDataDragIgnoresCompositorSwipes(t *testing.T) { | |
| 517 | + | d := startDnd(t) | |
| 518 | + | d.start(t, 0) | |
| 519 | + | before := d.settle(t) | |
| 520 | + | // Flicks that page columns and workspaces, as navigation would. | |
| 521 | + | for _, fingers := range []int{3, 4} { | |
| 522 | + | at := time.Second | |
| 523 | + | d.in(ports.SwipeBegin{Fingers: fingers, Time: at}) | |
| 524 | + | for range 10 { | |
| 525 | + | at += 8 * time.Millisecond | |
| 526 | + | d.in(ports.SwipeUpdate{DX: 60, AccelDX: 60, DY: 40, AccelDY: 40, Time: at}) | |
| 527 | + | } | |
| 528 | + | d.in(ports.SwipeEnd{Time: at}) | |
| 529 | + | } | |
| 530 | + | after := d.settle(t) | |
| 531 | + | if !slices.Equal(before[0].Windows, after[0].Windows) { | |
| 532 | + | t.Fatalf("a swipe moved windows during the drag:\n%#v\n%#v", before[0].Windows, after[0].Windows) | |
| 533 | + | } | |
| 534 | + | } | |
| @@ -463,3 +463,72 @@ func TestDataDragFocusScreenFollowsPointer(t *testing.T) { | |||||
| 463 | t.Fatalf("focus %+v", focus) | 463 | t.Fatalf("focus %+v", focus) | ||
| 464 | } | 464 | } | ||
| 465 | } | 465 | } | ||
| 466 | + | ||||
| 467 | + | func TestDataDragFollowsWorkspaceSwitchUnderStillCursor(t *testing.T) { | |||
| 468 | + | d := startDnd(t) | |||
| 469 | + | d.start(t, 0) | |||
| 470 | + | d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3) | |||
| 471 | + | d.settle(t) | |||
| 472 | + | d.drain() | |||
| 473 | + | // cmd+2 shows an empty workspace under the still cursor: the target | |||
| 474 | + | // leaves, and the release drops on nothing. | |||
| 475 | + | d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper, Pressed: true}) | |||
| 476 | + | d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper}) | |||
| 477 | + | d.settle(t) | |||
| 478 | + | got := dataDragCommands(d.drain()) | |||
| 479 | + | if !slices.Contains(got, ports.ClientCommand(ports.DataDragFocus{Time: 3 * time.Millisecond})) { | |||
| 480 | + | t.Fatalf("no leave after the switch: %#v", got) | |||
| 481 | + | } | |||
| 482 | + | d.in(ports.PointerButton{Button: 0x110, Time: 4 * time.Millisecond}) | |||
| 483 | + | d.settle(t) | |||
| 484 | + | got = dataDragCommands(d.drain()) | |||
| 485 | + | if len(got) == 0 || got[0] != (ports.DataDragDrop{}) { | |||
| 486 | + | t.Fatalf("commands %#v", got) | |||
| 487 | + | } | |||
| 488 | + | for _, v := range got { | |||
| 489 | + | if f, ok := v.(ports.DataDragFocus); ok && f.ID != 0 { | |||
| 490 | + | t.Fatalf("hidden window targeted: %#v", got) | |||
| 491 | + | } | |||
| 492 | + | } | |||
| 493 | + | } | |||
| 494 | + | ||||
| 495 | + | func TestDataDragEscapeCancelsOverSwitcher(t *testing.T) { | |||
| 496 | + | d := startDnd(t) | |||
| 497 | + | d.start(t, 0) | |||
| 498 | + | d.in(ports.KeyEvent{Keysym: "Super_L", Keycode: 125, Mods: ports.ModSuper, Pressed: true}) | |||
| 499 | + | d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper, Pressed: true}) | |||
| 500 | + | d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper}) | |||
| 501 | + | d.settle(t) | |||
| 502 | + | d.drain() | |||
| 503 | + | d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1, Mods: ports.ModSuper, Pressed: true}) | |||
| 504 | + | d.settle(t) | |||
| 505 | + | got := dataDragCommands(d.drain()) | |||
| 506 | + | if len(got) == 0 || got[0] != (ports.DataDragCancel{}) { | |||
| 507 | + | t.Fatalf("Escape did not cancel the drag: %#v", got) | |||
| 508 | + | } | |||
| 509 | + | d.in(ports.PointerButton{Button: 0x110}) | |||
| 510 | + | d.settle(t) | |||
| 511 | + | if slices.Contains(dataDragCommands(d.drain()), ports.ClientCommand(ports.DataDragDrop{})) { | |||
| 512 | + | t.Fatal("dropped after Escape") | |||
| 513 | + | } | |||
| 514 | + | } | |||
| 515 | + | ||||
| 516 | + | func TestDataDragIgnoresCompositorSwipes(t *testing.T) { | |||
| 517 | + | d := startDnd(t) | |||
| 518 | + | d.start(t, 0) | |||
| 519 | + | before := d.settle(t) | |||
| 520 | + | // Flicks that page columns and workspaces, as navigation would. | |||
| 521 | + | for _, fingers := range []int{3, 4} { | |||
| 522 | + | at := time.Second | |||
| 523 | + | d.in(ports.SwipeBegin{Fingers: fingers, Time: at}) | |||
| 524 | + | for range 10 { | |||
| 525 | + | at += 8 * time.Millisecond | |||
| 526 | + | d.in(ports.SwipeUpdate{DX: 60, AccelDX: 60, DY: 40, AccelDY: 40, Time: at}) | |||
| 527 | + | } | |||
| 528 | + | d.in(ports.SwipeEnd{Time: at}) | |||
| 529 | + | } | |||
| 530 | + | after := d.settle(t) | |||
| 531 | + | if !slices.Equal(before[0].Windows, after[0].Windows) { | |||
| 532 | + | t.Fatalf("a swipe moved windows during the drag:\n%#v\n%#v", before[0].Windows, after[0].Windows) | |||
| 533 | + | } | |||
| 534 | + | } | |||
M internal/core/input.go +18 -6
| @@ -26,6 +26,10 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 26 | 26 | case ports.PointerAxis: | |
| 27 | 27 | return c.pointerAxis(ctx, v) | |
| 28 | 28 | case ports.SwipeBegin: | |
| 29 | + | if c.dataDrag != nil && !forwardsSwipe(v.Fingers) { | |
| 30 | + | // The drag owns the pointer: no navigation under it. | |
| 31 | + | return nil | |
| 32 | + | } | |
| 29 | 33 | if forwardsSwipe(v.Fingers) { | |
| 30 | 34 | return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time) | |
| 31 | 35 | } | |
| @@ -252,16 +256,17 @@ func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error { | |||
| 252 | 256 | return err | |
| 253 | 257 | } | |
| 254 | 258 | } | |
| 259 | + | if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" { | |
| 260 | + | // Escape cancels the data drag first, even over the switcher; | |
| 261 | + | // its release goes nowhere either. | |
| 262 | + | c.pressed[heldKey(key)] = true | |
| 263 | + | return c.cancelDataDrag(ctx) | |
| 264 | + | } | |
| 255 | 265 | if c.switching() && key.Pressed && key.Keysym == "Escape" { | |
| 256 | 266 | // Escape cancels the switcher; its release goes nowhere either. | |
| 257 | 267 | c.pressed[heldKey(key)] = true | |
| 258 | 268 | return c.abortSwitcher(ctx) | |
| 259 | 269 | } | |
| 260 | - | if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" { | |
| 261 | - | // Escape cancels the data drag; its release goes nowhere either. | |
| 262 | - | c.pressed[heldKey(key)] = true | |
| 263 | - | return c.cancelDataDrag(ctx) | |
| 264 | - | } | |
| 265 | 270 | if c.drag != nil && key.Pressed && key.Keysym == "Escape" { | |
| 266 | 271 | // Escape cancels the drag; its release goes nowhere either. | |
| 267 | 272 | c.cancelDrag() | |
| @@ -385,7 +390,14 @@ func (c *Core) runBind(ctx context.Context, action Action) error { | |||
| 385 | 390 | return err | |
| 386 | 391 | } | |
| 387 | 392 | } | |
| 388 | - | return c.publish(ctx) | |
| 393 | + | if err := c.publish(ctx); err != nil { | |
| 394 | + | return err | |
| 395 | + | } | |
| 396 | + | if c.dataDrag != nil { | |
| 397 | + | // A data drag follows the layout under the still cursor. | |
| 398 | + | return c.rehit(ctx) | |
| 399 | + | } | |
| 400 | + | return nil | |
| 389 | 401 | } | |
| 390 | 402 | ||
| 391 | 403 | // runSpawn starts the programs of a spawn effect of an action applied to w: | |
| @@ -26,6 +26,10 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 26 | case ports.PointerAxis: | 26 | case ports.PointerAxis: | ||
| 27 | return c.pointerAxis(ctx, v) | 27 | return c.pointerAxis(ctx, v) | ||
| 28 | case ports.SwipeBegin: | 28 | case ports.SwipeBegin: | ||
| 29 | + | if c.dataDrag != nil && !forwardsSwipe(v.Fingers) { | |||
| 30 | + | // The drag owns the pointer: no navigation under it. | |||
| 31 | + | return nil | |||
| 32 | + | } | |||
| 29 | if forwardsSwipe(v.Fingers) { | 33 | if forwardsSwipe(v.Fingers) { | ||
| 30 | return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time) | 34 | return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time) | ||
| 31 | } | 35 | } | ||
| @@ -252,16 +256,17 @@ func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error { | |||||
| 252 | return err | 256 | return err | ||
| 253 | } | 257 | } | ||
| 254 | } | 258 | } | ||
| 259 | + | if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" { | |||
| 260 | + | // Escape cancels the data drag first, even over the switcher; | |||
| 261 | + | // its release goes nowhere either. | |||
| 262 | + | c.pressed[heldKey(key)] = true | |||
| 263 | + | return c.cancelDataDrag(ctx) | |||
| 264 | + | } | |||
| 255 | if c.switching() && key.Pressed && key.Keysym == "Escape" { | 265 | if c.switching() && key.Pressed && key.Keysym == "Escape" { | ||
| 256 | // Escape cancels the switcher; its release goes nowhere either. | 266 | // Escape cancels the switcher; its release goes nowhere either. | ||
| 257 | c.pressed[heldKey(key)] = true | 267 | c.pressed[heldKey(key)] = true | ||
| 258 | return c.abortSwitcher(ctx) | 268 | return c.abortSwitcher(ctx) | ||
| 259 | } | 269 | } | ||
| 260 | - | if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" { | |||
| 261 | - | // Escape cancels the data drag; its release goes nowhere either. | |||
| 262 | - | c.pressed[heldKey(key)] = true | |||
| 263 | - | return c.cancelDataDrag(ctx) | |||
| 264 | - | } | |||
| 265 | if c.drag != nil && key.Pressed && key.Keysym == "Escape" { | 270 | if c.drag != nil && key.Pressed && key.Keysym == "Escape" { | ||
| 266 | // Escape cancels the drag; its release goes nowhere either. | 271 | // Escape cancels the drag; its release goes nowhere either. | ||
| 267 | c.cancelDrag() | 272 | c.cancelDrag() | ||
| @@ -385,7 +390,14 @@ func (c *Core) runBind(ctx context.Context, action Action) error { | |||||
| 385 | return err | 390 | return err | ||
| 386 | } | 391 | } | ||
| 387 | } | 392 | } | ||
| 388 | - | return c.publish(ctx) | 393 | + | if err := c.publish(ctx); err != nil { |
| 394 | + | return err | |||
| 395 | + | } | |||
| 396 | + | if c.dataDrag != nil { | |||
| 397 | + | // A data drag follows the layout under the still cursor. | |||
| 398 | + | return c.rehit(ctx) | |||
| 399 | + | } | |||
| 400 | + | return nil | |||
| 389 | } | 401 | } | ||
| 390 | 402 | ||||
| 391 | // runSpawn starts the programs of a spawn effect of an action applied to w: | 403 | // runSpawn starts the programs of a spawn effect of an action applied to w: | ||
M internal/core/scene.go +1 -1
| @@ -21,7 +21,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 21 | 21 | // layout is what is drawn; settled (same indexes) is where the | |
| 22 | 22 | // windows are going, and alone sizes the configures. | |
| 23 | 23 | layout, settled := sc.shown, sc.settledLayout | |
| 24 | - | scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)} | |
| 24 | + | scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)+1), Layers: shownLayers(sc)} | |
| 25 | 25 | if !sc.mon.previewing() { | |
| 26 | 26 | // Tiles stop at the panels; the overview lays out previews over | |
| 27 | 27 | // the whole output. | |
| @@ -21,7 +21,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 21 | // layout is what is drawn; settled (same indexes) is where the | 21 | // layout is what is drawn; settled (same indexes) is where the | ||
| 22 | // windows are going, and alone sizes the configures. | 22 | // windows are going, and alone sizes the configures. | ||
| 23 | layout, settled := sc.shown, sc.settledLayout | 23 | layout, settled := sc.shown, sc.settledLayout | ||
| 24 | - | scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)} | 24 | + | scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)+1), Layers: shownLayers(sc)} |
| 25 | if !sc.mon.previewing() { | 25 | if !sc.mon.previewing() { | ||
| 26 | // Tiles stop at the panels; the overview lays out previews over | 26 | // Tiles stop at the panels; the overview lays out previews over | ||
| 27 | // the whole output. | 27 | // the whole output. | ||