fix(dnd): tie data drags to the implicit grab button and follow target moves
- commit
- e05cb150e25a0922622fbeca7a6ea6a57c732a37
- author
- brice · · 2026-10-17 15:15 UTC
- parent
- 3a8fe0c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email - refs
- feat/data-dnd
10 files · +115 -43
M internal/adapters/wayland/dnd.go +6 -7
| @@ -174,12 +174,11 @@ func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, or | |||
| 174 | 174 | iconSurf.role = func(bool) { s.dndIconRole(d, iconSurf) } | |
| 175 | 175 | } | |
| 176 | 176 | s.dnd = d | |
| 177 | - | // Core consumes the releases of a drag: the buttons are not held for | |
| 178 | - | // the next press. | |
| 179 | - | clear(s.seat.pointerHeld) | |
| 180 | - | s.seat.grabSerial = 0 | |
| 177 | + | // Core consumes the releases of a drag: the grab ends here. | |
| 178 | + | button := s.seat.grabButton | |
| 179 | + | s.seat.grabSerial, s.seat.grabButton = 0, 0 | |
| 181 | 180 | s.log.Info().Uint64("icon", uint64(d.iconID)).Bool("source", cs != nil).Msg("data drag started") | |
| 182 | - | s.emit(ports.DataDragStarted{Icon: d.iconID}) | |
| 181 | + | s.emit(ports.DataDragStarted{Button: button, Icon: d.iconID}) | |
| 183 | 182 | if iconSurf != nil && iconSurf.has { | |
| 184 | 183 | // The icon was committed before the drag: show what it holds. | |
| 185 | 184 | iconSurf.committed = damage{full: true} | |
| @@ -188,10 +187,10 @@ func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, or | |||
| 188 | 187 | } | |
| 189 | 188 | } | |
| 190 | 189 | ||
| 191 | - | // validGrab reports whether serial is the press of the pointer button still | |
| 190 | + | // validGrab reports whether serial is the press of the implicit grab still | |
| 192 | 191 | // held, on the surface with pointer focus (or a subsurface of its tree). | |
| 193 | 192 | func (s *Server) validGrab(origin *wayland.Surface, serial uint32) bool { | |
| 194 | - | if serial == 0 || serial != s.seat.grabSerial || len(s.seat.pointerHeld) == 0 { | |
| 193 | + | if serial == 0 || serial != s.seat.grabSerial { | |
| 195 | 194 | return false | |
| 196 | 195 | } | |
| 197 | 196 | focus, _, _, _ := s.pointerSurface(s.seat.pointerFocus, 0, 0) | |
| @@ -174,12 +174,11 @@ func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, or | |||||
| 174 | iconSurf.role = func(bool) { s.dndIconRole(d, iconSurf) } | 174 | iconSurf.role = func(bool) { s.dndIconRole(d, iconSurf) } | ||
| 175 | } | 175 | } | ||
| 176 | s.dnd = d | 176 | s.dnd = d | ||
| 177 | - | // Core consumes the releases of a drag: the buttons are not held for | 177 | + | // Core consumes the releases of a drag: the grab ends here. |
| 178 | - | // the next press. | 178 | + | button := s.seat.grabButton |
| 179 | - | clear(s.seat.pointerHeld) | 179 | + | s.seat.grabSerial, s.seat.grabButton = 0, 0 |
| 180 | - | s.seat.grabSerial = 0 | |||
| 181 | s.log.Info().Uint64("icon", uint64(d.iconID)).Bool("source", cs != nil).Msg("data drag started") | 180 | s.log.Info().Uint64("icon", uint64(d.iconID)).Bool("source", cs != nil).Msg("data drag started") | ||
| 182 | - | s.emit(ports.DataDragStarted{Icon: d.iconID}) | 181 | + | s.emit(ports.DataDragStarted{Button: button, Icon: d.iconID}) |
| 183 | if iconSurf != nil && iconSurf.has { | 182 | if iconSurf != nil && iconSurf.has { | ||
| 184 | // The icon was committed before the drag: show what it holds. | 183 | // The icon was committed before the drag: show what it holds. | ||
| 185 | iconSurf.committed = damage{full: true} | 184 | iconSurf.committed = damage{full: true} | ||
| @@ -188,10 +187,10 @@ func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, or | |||||
| 188 | } | 187 | } | ||
| 189 | } | 188 | } | ||
| 190 | 189 | ||||
| 191 | - | // validGrab reports whether serial is the press of the pointer button still | 190 | + | // validGrab reports whether serial is the press of the implicit grab still |
| 192 | // held, on the surface with pointer focus (or a subsurface of its tree). | 191 | // held, on the surface with pointer focus (or a subsurface of its tree). | ||
| 193 | func (s *Server) validGrab(origin *wayland.Surface, serial uint32) bool { | 192 | func (s *Server) validGrab(origin *wayland.Surface, serial uint32) bool { | ||
| 194 | - | if serial == 0 || serial != s.seat.grabSerial || len(s.seat.pointerHeld) == 0 { | 193 | + | if serial == 0 || serial != s.seat.grabSerial { |
| 195 | return false | 194 | return false | ||
| 196 | } | 195 | } | ||
| 197 | focus, _, _, _ := s.pointerSurface(s.seat.pointerFocus, 0, 0) | 196 | focus, _, _, _ := s.pointerSurface(s.seat.pointerFocus, 0, 0) | ||
M internal/adapters/wayland/dnd_test.go +24 -1
| @@ -232,7 +232,7 @@ func (d *dndClient) source(t *testing.T, actions uint32, mimes ...string) uint32 | |||
| 232 | 232 | func (d *dndClient) press(t *testing.T, commands chan<- ports.ClientCommand) uint32 { | |
| 233 | 233 | t.Helper() | |
| 234 | 234 | commands <- ports.PointerFocus{ID: d.window, X: 1, Y: 1} | |
| 235 | - | commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Pressed: true, Time: time.Millisecond} | |
| 235 | + | commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Pressed: true, Grab: true, Time: time.Millisecond} | |
| 236 | 236 | deadline := time.After(2 * time.Second) | |
| 237 | 237 | for { | |
| 238 | 238 | if err := d.c.Roundtrip(); err != nil { | |
| @@ -652,8 +652,31 @@ func TestDataDragInvalidStart(t *testing.T) { | |||
| 652 | 652 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, second) | |
| 653 | 653 | p.a.expect(t, "cancelled") | |
| 654 | 654 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, first) | |
| 655 | + | // Core is told the grab's button: its release drops. | |
| 656 | + | if ev := dndNextEvent[ports.DataDragStarted](t, p.events); ev.Button != 0x110 { | |
| 657 | + | t.Fatalf("drag button %#x, want the grab's", ev.Button) | |
| 658 | + | } | |
| 659 | + | }) | |
| 660 | + | t.Run("grab release consumed by core", func(t *testing.T) { | |
| 661 | + | // A window drag consumes the release: the next grab press | |
| 662 | + | // still makes the serial a drag can name. | |
| 663 | + | p := newDndPair(t) | |
| 664 | + | stale := p.a.press(t, p.commands) | |
| 665 | + | serial := p.a.press(t, p.commands) | |
| 666 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, stale) | |
| 667 | + | p.a.expect(t, "cancelled") | |
| 668 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |
| 655 | 669 | dndNextEvent[ports.DataDragStarted](t, p.events) | |
| 656 | 670 | }) | |
| 671 | + | t.Run("grab release of a gone window", func(t *testing.T) { | |
| 672 | + | p := newDndPair(t) | |
| 673 | + | serial := p.a.press(t, p.commands) | |
| 674 | + | p.commands <- ports.PointerButtonTo{ID: 9999, Button: 0x110, Time: 2 * time.Millisecond} | |
| 675 | + | p.a.key(t, p.commands) // the release is applied once the key is seen | |
| 676 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |
| 677 | + | p.a.expect(t, "cancelled") | |
| 678 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 679 | + | }) | |
| 657 | 680 | t.Run("used source", func(t *testing.T) { | |
| 658 | 681 | p := newDndPair(t) | |
| 659 | 682 | src := p.start(t, 0) | |
| @@ -232,7 +232,7 @@ func (d *dndClient) source(t *testing.T, actions uint32, mimes ...string) uint32 | |||||
| 232 | func (d *dndClient) press(t *testing.T, commands chan<- ports.ClientCommand) uint32 { | 232 | func (d *dndClient) press(t *testing.T, commands chan<- ports.ClientCommand) uint32 { | ||
| 233 | t.Helper() | 233 | t.Helper() | ||
| 234 | commands <- ports.PointerFocus{ID: d.window, X: 1, Y: 1} | 234 | commands <- ports.PointerFocus{ID: d.window, X: 1, Y: 1} | ||
| 235 | - | commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Pressed: true, Time: time.Millisecond} | 235 | + | commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Pressed: true, Grab: true, Time: time.Millisecond} |
| 236 | deadline := time.After(2 * time.Second) | 236 | deadline := time.After(2 * time.Second) | ||
| 237 | for { | 237 | for { | ||
| 238 | if err := d.c.Roundtrip(); err != nil { | 238 | if err := d.c.Roundtrip(); err != nil { | ||
| @@ -652,8 +652,31 @@ func TestDataDragInvalidStart(t *testing.T) { | |||||
| 652 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, second) | 652 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, second) | ||
| 653 | p.a.expect(t, "cancelled") | 653 | p.a.expect(t, "cancelled") | ||
| 654 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, first) | 654 | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, first) | ||
| 655 | + | // Core is told the grab's button: its release drops. | |||
| 656 | + | if ev := dndNextEvent[ports.DataDragStarted](t, p.events); ev.Button != 0x110 { | |||
| 657 | + | t.Fatalf("drag button %#x, want the grab's", ev.Button) | |||
| 658 | + | } | |||
| 659 | + | }) | |||
| 660 | + | t.Run("grab release consumed by core", func(t *testing.T) { | |||
| 661 | + | // A window drag consumes the release: the next grab press | |||
| 662 | + | // still makes the serial a drag can name. | |||
| 663 | + | p := newDndPair(t) | |||
| 664 | + | stale := p.a.press(t, p.commands) | |||
| 665 | + | serial := p.a.press(t, p.commands) | |||
| 666 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, stale) | |||
| 667 | + | p.a.expect(t, "cancelled") | |||
| 668 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |||
| 655 | dndNextEvent[ports.DataDragStarted](t, p.events) | 669 | dndNextEvent[ports.DataDragStarted](t, p.events) | ||
| 656 | }) | 670 | }) | ||
| 671 | + | t.Run("grab release of a gone window", func(t *testing.T) { | |||
| 672 | + | p := newDndPair(t) | |||
| 673 | + | serial := p.a.press(t, p.commands) | |||
| 674 | + | p.commands <- ports.PointerButtonTo{ID: 9999, Button: 0x110, Time: 2 * time.Millisecond} | |||
| 675 | + | p.a.key(t, p.commands) // the release is applied once the key is seen | |||
| 676 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |||
| 677 | + | p.a.expect(t, "cancelled") | |||
| 678 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |||
| 679 | + | }) | |||
| 657 | t.Run("used source", func(t *testing.T) { | 680 | t.Run("used source", func(t *testing.T) { | ||
| 658 | p := newDndPair(t) | 681 | p := newDndPair(t) | ||
| 659 | src := p.start(t, 0) | 682 | src := p.start(t, 0) | ||
M internal/adapters/wayland/seat.go +14 -16
| @@ -43,11 +43,13 @@ type seatState struct { | |||
| 43 | 43 | // pressClient: popup grabs must come from it. | |
| 44 | 44 | press uint32 | |
| 45 | 45 | pressClient server.Client | |
| 46 | - | // pointerHeld are the pointer buttons held, and grabSerial the serial of | |
| 47 | - | // the press that took the first one: the only serial start_drag accepts | |
| 48 | - | // (wlr_seat_validate_pointer_grab_serial). | |
| 49 | - | pointerHeld map[uint32]bool | |
| 50 | - | grabSerial uint32 | |
| 46 | + | // grabSerial is the serial of the press that started the implicit | |
| 47 | + | // grab (core marks it), grabButton its button: the only serial | |
| 48 | + | // start_drag accepts (wlr_seat_validate_pointer_grab_serial). The | |
| 49 | + | // grab's release clears it; core may consume that release (a window | |
| 50 | + | // drag), and then the next grab press replaces it. | |
| 51 | + | grabSerial uint32 | |
| 52 | + | grabButton uint32 | |
| 51 | 53 | // pressAt and focusAt date the last press and keyboard focus change, | |
| 52 | 54 | // for xdg-activation tokens. | |
| 53 | 55 | pressAt, focusAt time.Time | |
| @@ -132,23 +134,19 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||
| 132 | 134 | } | |
| 133 | 135 | case ports.PointerButtonTo: | |
| 134 | 136 | // Releases may target the implicit-grab window after focus has moved. | |
| 137 | + | if c.Pressed && c.Grab || !c.Pressed && c.Button == s.seat.grabButton { | |
| 138 | + | // A new grab, or the end of this one even when its window is | |
| 139 | + | // gone. | |
| 140 | + | s.seat.grabSerial, s.seat.grabButton = 0, 0 | |
| 141 | + | } | |
| 135 | 142 | if client, pointers, ok := s.pointerTarget(c.ID); ok { | |
| 136 | 143 | s.serial++ | |
| 137 | 144 | state := uint32(0) | |
| 138 | 145 | if c.Pressed { | |
| 139 | 146 | state = 1 | |
| 140 | 147 | s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now() | |
| 141 | - | if len(s.seat.pointerHeld) == 0 { | |
| 142 | - | s.seat.grabSerial = s.serial | |
| 143 | - | } | |
| 144 | - | if s.seat.pointerHeld == nil { | |
| 145 | - | s.seat.pointerHeld = make(map[uint32]bool) | |
| 146 | - | } | |
| 147 | - | s.seat.pointerHeld[c.Button] = true | |
| 148 | - | } else { | |
| 149 | - | delete(s.seat.pointerHeld, c.Button) | |
| 150 | - | if len(s.seat.pointerHeld) == 0 { | |
| 151 | - | s.seat.grabSerial = 0 | |
| 148 | + | if c.Grab { | |
| 149 | + | s.seat.grabSerial, s.seat.grabButton = s.serial, c.Button | |
| 152 | 150 | } | |
| 153 | 151 | } | |
| 154 | 152 | for _, p := range pointers { | |
| @@ -43,11 +43,13 @@ type seatState struct { | |||||
| 43 | // pressClient: popup grabs must come from it. | 43 | // pressClient: popup grabs must come from it. | ||
| 44 | press uint32 | 44 | press uint32 | ||
| 45 | pressClient server.Client | 45 | pressClient server.Client | ||
| 46 | - | // pointerHeld are the pointer buttons held, and grabSerial the serial of | 46 | + | // grabSerial is the serial of the press that started the implicit |
| 47 | - | // the press that took the first one: the only serial start_drag accepts | 47 | + | // grab (core marks it), grabButton its button: the only serial |
| 48 | - | // (wlr_seat_validate_pointer_grab_serial). | 48 | + | // start_drag accepts (wlr_seat_validate_pointer_grab_serial). The |
| 49 | - | pointerHeld map[uint32]bool | 49 | + | // grab's release clears it; core may consume that release (a window |
| 50 | - | grabSerial uint32 | 50 | + | // drag), and then the next grab press replaces it. |
| 51 | + | grabSerial uint32 | |||
| 52 | + | grabButton uint32 | |||
| 51 | // pressAt and focusAt date the last press and keyboard focus change, | 53 | // pressAt and focusAt date the last press and keyboard focus change, | ||
| 52 | // for xdg-activation tokens. | 54 | // for xdg-activation tokens. | ||
| 53 | pressAt, focusAt time.Time | 55 | pressAt, focusAt time.Time | ||
| @@ -132,23 +134,19 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||||
| 132 | } | 134 | } | ||
| 133 | case ports.PointerButtonTo: | 135 | case ports.PointerButtonTo: | ||
| 134 | // Releases may target the implicit-grab window after focus has moved. | 136 | // Releases may target the implicit-grab window after focus has moved. | ||
| 137 | + | if c.Pressed && c.Grab || !c.Pressed && c.Button == s.seat.grabButton { | |||
| 138 | + | // A new grab, or the end of this one even when its window is | |||
| 139 | + | // gone. | |||
| 140 | + | s.seat.grabSerial, s.seat.grabButton = 0, 0 | |||
| 141 | + | } | |||
| 135 | if client, pointers, ok := s.pointerTarget(c.ID); ok { | 142 | if client, pointers, ok := s.pointerTarget(c.ID); ok { | ||
| 136 | s.serial++ | 143 | s.serial++ | ||
| 137 | state := uint32(0) | 144 | state := uint32(0) | ||
| 138 | if c.Pressed { | 145 | if c.Pressed { | ||
| 139 | state = 1 | 146 | state = 1 | ||
| 140 | s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now() | 147 | s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now() | ||
| 141 | - | if len(s.seat.pointerHeld) == 0 { | 148 | + | if c.Grab { |
| 142 | - | s.seat.grabSerial = s.serial | 149 | + | s.seat.grabSerial, s.seat.grabButton = s.serial, c.Button |
| 143 | - | } | |||
| 144 | - | if s.seat.pointerHeld == nil { | |||
| 145 | - | s.seat.pointerHeld = make(map[uint32]bool) | |||
| 146 | - | } | |||
| 147 | - | s.seat.pointerHeld[c.Button] = true | |||
| 148 | - | } else { | |||
| 149 | - | delete(s.seat.pointerHeld, c.Button) | |||
| 150 | - | if len(s.seat.pointerHeld) == 0 { | |||
| 151 | - | s.seat.grabSerial = 0 | |||
| 152 | } | 150 | } | ||
| 153 | } | 151 | } | ||
| 154 | for _, p := range pointers { | 152 | for _, p := range pointers { | ||
M internal/adapters/wayland/session_lock.go +1 -2
| @@ -147,8 +147,7 @@ func (s *Server) resetProtectionInput() { | |||
| 147 | 147 | clear(s.seat.grabKeys) | |
| 148 | 148 | s.seat.modState = ports.ModState{} | |
| 149 | 149 | s.seat.press, s.seat.pressClient = 0, server.Client{} | |
| 150 | - | clear(s.seat.pointerHeld) | |
| 151 | - | s.seat.grabSerial = 0 | |
| 150 | + | s.seat.grabSerial, s.seat.grabButton = 0, 0 | |
| 152 | 151 | s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{} | |
| 153 | 152 | s.cursorSurface = nil | |
| 154 | 153 | } | |
| @@ -147,8 +147,7 @@ func (s *Server) resetProtectionInput() { | |||||
| 147 | clear(s.seat.grabKeys) | 147 | clear(s.seat.grabKeys) | ||
| 148 | s.seat.modState = ports.ModState{} | 148 | s.seat.modState = ports.ModState{} | ||
| 149 | s.seat.press, s.seat.pressClient = 0, server.Client{} | 149 | s.seat.press, s.seat.pressClient = 0, server.Client{} | ||
| 150 | - | clear(s.seat.pointerHeld) | 150 | + | s.seat.grabSerial, s.seat.grabButton = 0, 0 |
| 151 | - | s.seat.grabSerial = 0 | |||
| 152 | s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{} | 151 | s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{} | ||
| 153 | s.cursorSurface = nil | 152 | s.cursorSurface = nil | ||
| 154 | } | 153 | } | ||
M internal/core/core_test.go +1 -1
| @@ -307,7 +307,7 @@ func TestPointerFocusAndGrab(t *testing.T) { | |||
| 307 | 307 | t.Fatal(v) | |
| 308 | 308 | } | |
| 309 | 309 | input <- ports.PointerButton{Button: 0x110, Pressed: true} | |
| 310 | - | if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true}) { | |
| 310 | + | if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true, Grab: true}) { | |
| 311 | 311 | t.Fatal(v) | |
| 312 | 312 | } | |
| 313 | 313 | scene(t, scenes) | |
| @@ -307,7 +307,7 @@ func TestPointerFocusAndGrab(t *testing.T) { | |||||
| 307 | t.Fatal(v) | 307 | t.Fatal(v) | ||
| 308 | } | 308 | } | ||
| 309 | input <- ports.PointerButton{Button: 0x110, Pressed: true} | 309 | input <- ports.PointerButton{Button: 0x110, Pressed: true} | ||
| 310 | - | if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true}) { | 310 | + | if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true, Grab: true}) { |
| 311 | t.Fatal(v) | 311 | t.Fatal(v) | ||
| 312 | } | 312 | } | ||
| 313 | scene(t, scenes) | 313 | scene(t, scenes) | ||
M internal/core/datadrag.go +9 -5
| @@ -31,8 +31,10 @@ import ( | |||
| 31 | 31 | type dataDragState struct { | |
| 32 | 32 | // button is the pointer button that holds the drag. | |
| 33 | 33 | button uint32 | |
| 34 | - | // target is the surface the drag was last routed to (0: none). | |
| 34 | + | // target is the surface the drag was last routed to (0: none), at | |
| 35 | + | // at in its coordinates. | |
| 35 | 36 | target WindowID | |
| 37 | + | at [2]float64 | |
| 36 | 38 | icon dataDragIcon | |
| 37 | 39 | } | |
| 38 | 40 | ||
| @@ -47,7 +49,7 @@ type dataDragIcon struct { | |||
| 47 | 49 | // pointer if a client holds a button, else cancels the drag. A drag | |
| 48 | 50 | // during a window drag or a locked session is refused. | |
| 49 | 51 | func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error { | |
| 50 | - | button := c.lastButton | |
| 52 | + | button := v.Button | |
| 51 | 53 | if c.dataDrag != nil || c.drag != nil || c.security.Protected || c.grab == 0 || c.held() == 0 || !c.buttons[button] || c.swallow[button] { | |
| 52 | 54 | return c.command(ctx, ports.DataDragCancel{}) | |
| 53 | 55 | } | |
| @@ -81,6 +83,7 @@ func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error | |||
| 81 | 83 | func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error { | |
| 82 | 84 | d := c.dataDrag | |
| 83 | 85 | id, x, y := c.hit(c.cursorX, c.cursorY) | |
| 86 | + | d.at = [2]float64{x, y} | |
| 84 | 87 | switch { | |
| 85 | 88 | case id != d.target: | |
| 86 | 89 | d.target = id | |
| @@ -91,10 +94,11 @@ func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error { | |||
| 91 | 94 | return nil | |
| 92 | 95 | } | |
| 93 | 96 | ||
| 94 | - | // dataDragRetarget re-routes a still cursor: only a changed target is | |
| 95 | - | // told. | |
| 97 | + | // dataDragRetarget re-routes a still cursor: the target is told only when | |
| 98 | + | // it or the point in it changed. | |
| 96 | 99 | func (c *Core) dataDragRetarget(ctx context.Context, at time.Duration) error { | |
| 97 | - | if id, _, _ := c.hit(c.cursorX, c.cursorY); id == c.dataDrag.target { | |
| 100 | + | d := c.dataDrag | |
| 101 | + | if id, x, y := c.hit(c.cursorX, c.cursorY); id == d.target && (id == 0 || d.at == [2]float64{x, y}) { | |
| 98 | 102 | return nil | |
| 99 | 103 | } | |
| 100 | 104 | return c.dataDragRoute(ctx, at) | |
| @@ -31,8 +31,10 @@ import ( | |||||
| 31 | type dataDragState struct { | 31 | type dataDragState struct { | ||
| 32 | // button is the pointer button that holds the drag. | 32 | // button is the pointer button that holds the drag. | ||
| 33 | button uint32 | 33 | button uint32 | ||
| 34 | - | // target is the surface the drag was last routed to (0: none). | 34 | + | // target is the surface the drag was last routed to (0: none), at |
| 35 | + | // at in its coordinates. | |||
| 35 | target WindowID | 36 | target WindowID | ||
| 37 | + | at [2]float64 | |||
| 36 | icon dataDragIcon | 38 | icon dataDragIcon | ||
| 37 | } | 39 | } | ||
| 38 | 40 | ||||
| @@ -47,7 +49,7 @@ type dataDragIcon struct { | |||||
| 47 | // pointer if a client holds a button, else cancels the drag. A drag | 49 | // pointer if a client holds a button, else cancels the drag. A drag | ||
| 48 | // during a window drag or a locked session is refused. | 50 | // during a window drag or a locked session is refused. | ||
| 49 | func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error { | 51 | func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error { | ||
| 50 | - | button := c.lastButton | 52 | + | button := v.Button |
| 51 | if c.dataDrag != nil || c.drag != nil || c.security.Protected || c.grab == 0 || c.held() == 0 || !c.buttons[button] || c.swallow[button] { | 53 | if c.dataDrag != nil || c.drag != nil || c.security.Protected || c.grab == 0 || c.held() == 0 || !c.buttons[button] || c.swallow[button] { | ||
| 52 | return c.command(ctx, ports.DataDragCancel{}) | 54 | return c.command(ctx, ports.DataDragCancel{}) | ||
| 53 | } | 55 | } | ||
| @@ -81,6 +83,7 @@ func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error | |||||
| 81 | func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error { | 83 | func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error { | ||
| 82 | d := c.dataDrag | 84 | d := c.dataDrag | ||
| 83 | id, x, y := c.hit(c.cursorX, c.cursorY) | 85 | id, x, y := c.hit(c.cursorX, c.cursorY) | ||
| 86 | + | d.at = [2]float64{x, y} | |||
| 84 | switch { | 87 | switch { | ||
| 85 | case id != d.target: | 88 | case id != d.target: | ||
| 86 | d.target = id | 89 | d.target = id | ||
| @@ -91,10 +94,11 @@ func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error { | |||||
| 91 | return nil | 94 | return nil | ||
| 92 | } | 95 | } | ||
| 93 | 96 | ||||
| 94 | - | // dataDragRetarget re-routes a still cursor: only a changed target is | 97 | + | // dataDragRetarget re-routes a still cursor: the target is told only when |
| 95 | - | // told. | 98 | + | // it or the point in it changed. |
| 96 | func (c *Core) dataDragRetarget(ctx context.Context, at time.Duration) error { | 99 | func (c *Core) dataDragRetarget(ctx context.Context, at time.Duration) error { | ||
| 97 | - | if id, _, _ := c.hit(c.cursorX, c.cursorY); id == c.dataDrag.target { | 100 | + | d := c.dataDrag |
| 101 | + | if id, x, y := c.hit(c.cursorX, c.cursorY); id == d.target && (id == 0 || d.at == [2]float64{x, y}) { | |||
| 98 | return nil | 102 | return nil | ||
| 99 | } | 103 | } | ||
| 100 | return c.dataDragRoute(ctx, at) | 104 | return c.dataDragRoute(ctx, at) | ||
M internal/core/datadrag_test.go +49 -6
| @@ -51,7 +51,7 @@ func dataDragCommands(cmds []ports.ClientCommand) []ports.ClientCommand { | |||
| 51 | 51 | ||
| 52 | 52 | func (d *dndRig) start(t *testing.T, icon ports.WindowID) []ports.ClientCommand { | |
| 53 | 53 | t.Helper() | |
| 54 | - | d.client <- ports.DataDragStarted{Icon: icon} | |
| 54 | + | d.client <- ports.DataDragStarted{Button: 0x110, Icon: icon} | |
| 55 | 55 | d.settle(t) | |
| 56 | 56 | return dataDragCommands(d.drain()) | |
| 57 | 57 | } | |
| @@ -108,7 +108,7 @@ func TestDataDragDropsOnRelease(t *testing.T) { | |||
| 108 | 108 | // The next press is an ordinary click. | |
| 109 | 109 | d.in(ports.PointerButton{Button: 0x110, Pressed: true}) | |
| 110 | 110 | d.settle(t) | |
| 111 | - | wantCommands(t, dataDragCommands(d.drain()), ports.PointerButtonTo{ID: 2, Button: 0x110, Pressed: true}) | |
| 111 | + | wantCommands(t, dataDragCommands(d.drain()), ports.PointerButtonTo{ID: 2, Button: 0x110, Pressed: true, Grab: true}) | |
| 112 | 112 | } | |
| 113 | 113 | ||
| 114 | 114 | func TestDataDragSendsNothingToClients(t *testing.T) { | |
| @@ -231,7 +231,7 @@ func TestDataDragRefused(t *testing.T) { | |||
| 231 | 231 | r.press(t, 0x110, ports.ModSuper) | |
| 232 | 232 | r.settle(t) | |
| 233 | 233 | r.drain() | |
| 234 | - | r.client <- ports.DataDragStarted{} | |
| 234 | + | r.client <- ports.DataDragStarted{Button: 0x110} | |
| 235 | 235 | r.settle(t) | |
| 236 | 236 | wantCommands(t, r.drain(), ports.DataDragCancel{}) | |
| 237 | 237 | // The window drag goes on. | |
| @@ -252,7 +252,7 @@ func TestDataDragRefused(t *testing.T) { | |||
| 252 | 252 | d.client <- ports.SessionLockChanged{State: locked} | |
| 253 | 253 | d.settle(t) | |
| 254 | 254 | d.drain() | |
| 255 | - | d.client <- ports.DataDragStarted{} | |
| 255 | + | d.client <- ports.DataDragStarted{Button: 0x110} | |
| 256 | 256 | d.settle(t) | |
| 257 | 257 | var got []ports.ClientCommand | |
| 258 | 258 | for _, v := range d.drain() { | |
| @@ -267,7 +267,7 @@ func TestDataDragRefused(t *testing.T) { | |||
| 267 | 267 | ||
| 268 | 268 | func TestDataDragCommandsAreSecurityWrapped(t *testing.T) { | |
| 269 | 269 | d := startDnd(t) | |
| 270 | - | d.client <- ports.DataDragStarted{} | |
| 270 | + | d.client <- ports.DataDragStarted{Button: 0x110} | |
| 271 | 271 | d.settle(t) | |
| 272 | 272 | n := 0 | |
| 273 | 273 | for { | |
| @@ -446,7 +446,7 @@ func TestDataDragFocusScreenFollowsPointer(t *testing.T) { | |||
| 446 | 446 | } | |
| 447 | 447 | r.in(ports.PointerButton{Button: 0x110, Pressed: true}) | |
| 448 | 448 | r.settle(t) | |
| 449 | - | r.client <- ports.DataDragStarted{} | |
| 449 | + | r.client <- ports.DataDragStarted{Button: 0x110} | |
| 450 | 450 | r.moveTo(1500, 300, 3) | |
| 451 | 451 | set = r.settle(t) | |
| 452 | 452 | if w, _ := windowIn(set, "B", 3); !w.Focused { | |
| @@ -492,6 +492,49 @@ func TestDataDragFollowsWorkspaceSwitchUnderStillCursor(t *testing.T) { | |||
| 492 | 492 | } | |
| 493 | 493 | } | |
| 494 | 494 | ||
| 495 | + | func TestDataDragTargetMovedUnderStillCursor(t *testing.T) { | |
| 496 | + | d := startDnd(t) | |
| 497 | + | d.start(t, 0) | |
| 498 | + | // Over 2, focused by a click-free keyboard move, then cmd+f maximizes | |
| 499 | + | // its column: still the target under the cursor, at another point. | |
| 500 | + | d.moveTo(float64(d.two.X+d.two.W-20), float64(d.two.Y+30), 2) | |
| 501 | + | d.in(ports.KeyEvent{Keysym: "l", Keycode: 38, Mods: ports.ModSuper, Pressed: true}) | |
| 502 | + | d.in(ports.KeyEvent{Keysym: "l", Keycode: 38, Mods: ports.ModSuper}) | |
| 503 | + | d.settle(t) | |
| 504 | + | d.drain() | |
| 505 | + | d.in(ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper, Pressed: true}) | |
| 506 | + | d.in(ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper}) | |
| 507 | + | set := d.settle(t) | |
| 508 | + | wide := dragRect(t, set, dragOut.Name, 2) | |
| 509 | + | if wide.X == d.two.X { | |
| 510 | + | t.Fatalf("the column did not move: %v", wide) | |
| 511 | + | } | |
| 512 | + | x, y := float64(d.two.X+d.two.W-20-wide.X), float64(d.two.Y+30-wide.Y) | |
| 513 | + | got := dataDragCommands(d.drain()) | |
| 514 | + | if !slices.Contains(got, ports.ClientCommand(ports.DataDragMotion{ID: 2, X: x, Y: y, Time: 2 * time.Millisecond})) { | |
| 515 | + | t.Fatalf("no motion to %v,%v: %#v", x, y, got) | |
| 516 | + | } | |
| 517 | + | } | |
| 518 | + | ||
| 519 | + | func TestDataDragDropsOnItsOwnButton(t *testing.T) { | |
| 520 | + | d := startDnd(t) | |
| 521 | + | // Right pressed after left: the drag is left's, the grab's. | |
| 522 | + | d.in(ports.PointerButton{Button: 0x111, Pressed: true}) | |
| 523 | + | d.settle(t) | |
| 524 | + | d.drain() | |
| 525 | + | d.start(t, 0) | |
| 526 | + | d.in(ports.PointerButton{Button: 0x111}) | |
| 527 | + | d.settle(t) | |
| 528 | + | if got := dataDragCommands(d.drain()); slices.Contains(got, ports.ClientCommand(ports.DataDragDrop{})) { | |
| 529 | + | t.Fatalf("right's release dropped: %#v", got) | |
| 530 | + | } | |
| 531 | + | d.in(ports.PointerButton{Button: 0x110}) | |
| 532 | + | d.settle(t) | |
| 533 | + | if got := dataDragCommands(d.drain()); len(got) == 0 || got[0] != (ports.DataDragDrop{}) { | |
| 534 | + | t.Fatalf("left's release did not drop: %#v", got) | |
| 535 | + | } | |
| 536 | + | } | |
| 537 | + | ||
| 495 | 538 | func TestDataDragEscapeCancelsOverSwitcher(t *testing.T) { | |
| 496 | 539 | d := startDnd(t) | |
| 497 | 540 | d.start(t, 0) | |
| @@ -51,7 +51,7 @@ func dataDragCommands(cmds []ports.ClientCommand) []ports.ClientCommand { | |||||
| 51 | 51 | ||||
| 52 | func (d *dndRig) start(t *testing.T, icon ports.WindowID) []ports.ClientCommand { | 52 | func (d *dndRig) start(t *testing.T, icon ports.WindowID) []ports.ClientCommand { | ||
| 53 | t.Helper() | 53 | t.Helper() | ||
| 54 | - | d.client <- ports.DataDragStarted{Icon: icon} | 54 | + | d.client <- ports.DataDragStarted{Button: 0x110, Icon: icon} |
| 55 | d.settle(t) | 55 | d.settle(t) | ||
| 56 | return dataDragCommands(d.drain()) | 56 | return dataDragCommands(d.drain()) | ||
| 57 | } | 57 | } | ||
| @@ -108,7 +108,7 @@ func TestDataDragDropsOnRelease(t *testing.T) { | |||||
| 108 | // The next press is an ordinary click. | 108 | // The next press is an ordinary click. | ||
| 109 | d.in(ports.PointerButton{Button: 0x110, Pressed: true}) | 109 | d.in(ports.PointerButton{Button: 0x110, Pressed: true}) | ||
| 110 | d.settle(t) | 110 | d.settle(t) | ||
| 111 | - | wantCommands(t, dataDragCommands(d.drain()), ports.PointerButtonTo{ID: 2, Button: 0x110, Pressed: true}) | 111 | + | wantCommands(t, dataDragCommands(d.drain()), ports.PointerButtonTo{ID: 2, Button: 0x110, Pressed: true, Grab: true}) |
| 112 | } | 112 | } | ||
| 113 | 113 | ||||
| 114 | func TestDataDragSendsNothingToClients(t *testing.T) { | 114 | func TestDataDragSendsNothingToClients(t *testing.T) { | ||
| @@ -231,7 +231,7 @@ func TestDataDragRefused(t *testing.T) { | |||||
| 231 | r.press(t, 0x110, ports.ModSuper) | 231 | r.press(t, 0x110, ports.ModSuper) | ||
| 232 | r.settle(t) | 232 | r.settle(t) | ||
| 233 | r.drain() | 233 | r.drain() | ||
| 234 | - | r.client <- ports.DataDragStarted{} | 234 | + | r.client <- ports.DataDragStarted{Button: 0x110} |
| 235 | r.settle(t) | 235 | r.settle(t) | ||
| 236 | wantCommands(t, r.drain(), ports.DataDragCancel{}) | 236 | wantCommands(t, r.drain(), ports.DataDragCancel{}) | ||
| 237 | // The window drag goes on. | 237 | // The window drag goes on. | ||
| @@ -252,7 +252,7 @@ func TestDataDragRefused(t *testing.T) { | |||||
| 252 | d.client <- ports.SessionLockChanged{State: locked} | 252 | d.client <- ports.SessionLockChanged{State: locked} | ||
| 253 | d.settle(t) | 253 | d.settle(t) | ||
| 254 | d.drain() | 254 | d.drain() | ||
| 255 | - | d.client <- ports.DataDragStarted{} | 255 | + | d.client <- ports.DataDragStarted{Button: 0x110} |
| 256 | d.settle(t) | 256 | d.settle(t) | ||
| 257 | var got []ports.ClientCommand | 257 | var got []ports.ClientCommand | ||
| 258 | for _, v := range d.drain() { | 258 | for _, v := range d.drain() { | ||
| @@ -267,7 +267,7 @@ func TestDataDragRefused(t *testing.T) { | |||||
| 267 | 267 | ||||
| 268 | func TestDataDragCommandsAreSecurityWrapped(t *testing.T) { | 268 | func TestDataDragCommandsAreSecurityWrapped(t *testing.T) { | ||
| 269 | d := startDnd(t) | 269 | d := startDnd(t) | ||
| 270 | - | d.client <- ports.DataDragStarted{} | 270 | + | d.client <- ports.DataDragStarted{Button: 0x110} |
| 271 | d.settle(t) | 271 | d.settle(t) | ||
| 272 | n := 0 | 272 | n := 0 | ||
| 273 | for { | 273 | for { | ||
| @@ -446,7 +446,7 @@ func TestDataDragFocusScreenFollowsPointer(t *testing.T) { | |||||
| 446 | } | 446 | } | ||
| 447 | r.in(ports.PointerButton{Button: 0x110, Pressed: true}) | 447 | r.in(ports.PointerButton{Button: 0x110, Pressed: true}) | ||
| 448 | r.settle(t) | 448 | r.settle(t) | ||
| 449 | - | r.client <- ports.DataDragStarted{} | 449 | + | r.client <- ports.DataDragStarted{Button: 0x110} |
| 450 | r.moveTo(1500, 300, 3) | 450 | r.moveTo(1500, 300, 3) | ||
| 451 | set = r.settle(t) | 451 | set = r.settle(t) | ||
| 452 | if w, _ := windowIn(set, "B", 3); !w.Focused { | 452 | if w, _ := windowIn(set, "B", 3); !w.Focused { | ||
| @@ -492,6 +492,49 @@ func TestDataDragFollowsWorkspaceSwitchUnderStillCursor(t *testing.T) { | |||||
| 492 | } | 492 | } | ||
| 493 | } | 493 | } | ||
| 494 | 494 | ||||
| 495 | + | func TestDataDragTargetMovedUnderStillCursor(t *testing.T) { | |||
| 496 | + | d := startDnd(t) | |||
| 497 | + | d.start(t, 0) | |||
| 498 | + | // Over 2, focused by a click-free keyboard move, then cmd+f maximizes | |||
| 499 | + | // its column: still the target under the cursor, at another point. | |||
| 500 | + | d.moveTo(float64(d.two.X+d.two.W-20), float64(d.two.Y+30), 2) | |||
| 501 | + | d.in(ports.KeyEvent{Keysym: "l", Keycode: 38, Mods: ports.ModSuper, Pressed: true}) | |||
| 502 | + | d.in(ports.KeyEvent{Keysym: "l", Keycode: 38, Mods: ports.ModSuper}) | |||
| 503 | + | d.settle(t) | |||
| 504 | + | d.drain() | |||
| 505 | + | d.in(ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper, Pressed: true}) | |||
| 506 | + | d.in(ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper}) | |||
| 507 | + | set := d.settle(t) | |||
| 508 | + | wide := dragRect(t, set, dragOut.Name, 2) | |||
| 509 | + | if wide.X == d.two.X { | |||
| 510 | + | t.Fatalf("the column did not move: %v", wide) | |||
| 511 | + | } | |||
| 512 | + | x, y := float64(d.two.X+d.two.W-20-wide.X), float64(d.two.Y+30-wide.Y) | |||
| 513 | + | got := dataDragCommands(d.drain()) | |||
| 514 | + | if !slices.Contains(got, ports.ClientCommand(ports.DataDragMotion{ID: 2, X: x, Y: y, Time: 2 * time.Millisecond})) { | |||
| 515 | + | t.Fatalf("no motion to %v,%v: %#v", x, y, got) | |||
| 516 | + | } | |||
| 517 | + | } | |||
| 518 | + | ||||
| 519 | + | func TestDataDragDropsOnItsOwnButton(t *testing.T) { | |||
| 520 | + | d := startDnd(t) | |||
| 521 | + | // Right pressed after left: the drag is left's, the grab's. | |||
| 522 | + | d.in(ports.PointerButton{Button: 0x111, Pressed: true}) | |||
| 523 | + | d.settle(t) | |||
| 524 | + | d.drain() | |||
| 525 | + | d.start(t, 0) | |||
| 526 | + | d.in(ports.PointerButton{Button: 0x111}) | |||
| 527 | + | d.settle(t) | |||
| 528 | + | if got := dataDragCommands(d.drain()); slices.Contains(got, ports.ClientCommand(ports.DataDragDrop{})) { | |||
| 529 | + | t.Fatalf("right's release dropped: %#v", got) | |||
| 530 | + | } | |||
| 531 | + | d.in(ports.PointerButton{Button: 0x110}) | |||
| 532 | + | d.settle(t) | |||
| 533 | + | if got := dataDragCommands(d.drain()); len(got) == 0 || got[0] != (ports.DataDragDrop{}) { | |||
| 534 | + | t.Fatalf("left's release did not drop: %#v", got) | |||
| 535 | + | } | |||
| 536 | + | } | |||
| 537 | + | ||||
| 495 | func TestDataDragEscapeCancelsOverSwitcher(t *testing.T) { | 538 | func TestDataDragEscapeCancelsOverSwitcher(t *testing.T) { | ||
| 496 | d := startDnd(t) | 539 | d := startDnd(t) | ||
| 497 | d.start(t, 0) | 540 | d.start(t, 0) | ||
M internal/core/input.go +3 -2
| @@ -172,8 +172,9 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error { | |||
| 172 | 172 | return err | |
| 173 | 173 | } | |
| 174 | 174 | } | |
| 175 | + | grab := v.Pressed && c.held() == 0 | |
| 175 | 176 | if v.Pressed { | |
| 176 | - | if c.held() == 0 { | |
| 177 | + | if grab { | |
| 177 | 178 | c.grab = id | |
| 178 | 179 | } | |
| 179 | 180 | c.buttons[v.Button] = true | |
| @@ -182,7 +183,7 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error { | |||
| 182 | 183 | delete(c.buttons, v.Button) | |
| 183 | 184 | } | |
| 184 | 185 | if id != 0 { | |
| 185 | - | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil { | |
| 186 | + | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Grab: grab, Time: v.Time}); err != nil { | |
| 186 | 187 | return err | |
| 187 | 188 | } | |
| 188 | 189 | // A click on an on-demand layer gives it the keyboard. | |
| @@ -172,8 +172,9 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error { | |||||
| 172 | return err | 172 | return err | ||
| 173 | } | 173 | } | ||
| 174 | } | 174 | } | ||
| 175 | + | grab := v.Pressed && c.held() == 0 | |||
| 175 | if v.Pressed { | 176 | if v.Pressed { | ||
| 176 | - | if c.held() == 0 { | 177 | + | if grab { |
| 177 | c.grab = id | 178 | c.grab = id | ||
| 178 | } | 179 | } | ||
| 179 | c.buttons[v.Button] = true | 180 | c.buttons[v.Button] = true | ||
| @@ -182,7 +183,7 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error { | |||||
| 182 | delete(c.buttons, v.Button) | 183 | delete(c.buttons, v.Button) | ||
| 183 | } | 184 | } | ||
| 184 | if id != 0 { | 185 | if id != 0 { | ||
| 185 | - | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil { | 186 | + | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Grab: grab, Time: v.Time}); err != nil { |
| 186 | return err | 187 | return err | ||
| 187 | } | 188 | } | ||
| 188 | // A click on an on-demand layer gives it the keyboard. | 189 | // A click on an on-demand layer gives it the keyboard. | ||
M internal/ports/client_commands.go +3 -0
| @@ -178,6 +178,9 @@ type PointerButtonTo struct { | |||
| 178 | 178 | ID WindowID | |
| 179 | 179 | Button uint32 | |
| 180 | 180 | Pressed bool | |
| 181 | + | // Grab marks the press that starts the implicit grab: no other button | |
| 182 | + | // was held. start_drag must name its serial. | |
| 183 | + | Grab bool | |
| 181 | 184 | // Time is the device timestamp (CLOCK_MONOTONIC). | |
| 182 | 185 | Time time.Duration | |
| 183 | 186 | } | |
| @@ -178,6 +178,9 @@ type PointerButtonTo struct { | |||||
| 178 | ID WindowID | 178 | ID WindowID | ||
| 179 | Button uint32 | 179 | Button uint32 | ||
| 180 | Pressed bool | 180 | Pressed bool | ||
| 181 | + | // Grab marks the press that starts the implicit grab: no other button | |||
| 182 | + | // was held. start_drag must name its serial. | |||
| 183 | + | Grab bool | |||
| 181 | // Time is the device timestamp (CLOCK_MONOTONIC). | 184 | // Time is the device timestamp (CLOCK_MONOTONIC). | ||
| 182 | Time time.Duration | 185 | Time time.Duration | ||
| 183 | } | 186 | } | ||
M internal/ports/dnd.go +5 -3
| @@ -10,10 +10,12 @@ import "time" | |||
| 10 | 10 | // everything at the cursor. | |
| 11 | 11 | ||
| 12 | 12 | // DataDragStarted carries wayland → core a client drag answering the | |
| 13 | - | // client's current press. Icon is the drag icon's ID (0: none); core | |
| 14 | - | // answers with DataDragCancel when it refuses the drag. | |
| 13 | + | // client's current press. Button is the button of that press, whose | |
| 14 | + | // release drops; Icon is the drag icon's ID (0: none). Core answers with | |
| 15 | + | // DataDragCancel when it refuses the drag. | |
| 15 | 16 | type DataDragStarted struct { | |
| 16 | - | Icon WindowID | |
| 17 | + | Button uint32 | |
| 18 | + | Icon WindowID | |
| 17 | 19 | } | |
| 18 | 20 | ||
| 19 | 21 | func (DataDragStarted) clientEvent() {} | |
| @@ -10,10 +10,12 @@ import "time" | |||||
| 10 | // everything at the cursor. | 10 | // everything at the cursor. | ||
| 11 | 11 | ||||
| 12 | // DataDragStarted carries wayland → core a client drag answering the | 12 | // DataDragStarted carries wayland → core a client drag answering the | ||
| 13 | - | // client's current press. Icon is the drag icon's ID (0: none); core | 13 | + | // client's current press. Button is the button of that press, whose |
| 14 | - | // answers with DataDragCancel when it refuses the drag. | 14 | + | // release drops; Icon is the drag icon's ID (0: none). Core answers with |
| 15 | + | // DataDragCancel when it refuses the drag. | |||
| 15 | type DataDragStarted struct { | 16 | type DataDragStarted struct { | ||
| 16 | - | Icon WindowID | 17 | + | Button uint32 |
| 18 | + | Icon WindowID | |||
| 17 | } | 19 | } | ||
| 18 | 20 | ||||
| 19 | func (DataDragStarted) clientEvent() {} | 21 | func (DataDragStarted) clientEvent() {} | ||