feat(wayland): implement wl_data_device drag and drop
- commit
- a976009b8e0cace1c9df0a8c62c2be97555d8997
- author
- brice · · 2026-10-17 14:53 UTC
- parent
- 4634bd8
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +1414 -23
M docs/desktop.md +1 -1
| @@ -38,7 +38,7 @@ bind.cmd+v = spawn cliphist-pick | |||
| 38 | 38 | cliphist list | fuzzel --dmenu | cliphist decode | wl-copy | |
| 39 | 39 | ``` | |
| 40 | 40 | ||
| 41 | - | Drag and drop is not supported yet. | |
| 41 | + | Drag and drop works between clients: the surface under the pointer gets the offer, the drop and the chosen action (copy, move or ask, from the modifiers the clients read), and the drag icon follows the cursor. Escape, a refused drop, a dying source or the session lock cancel the drag. | |
| 42 | 42 | ||
| 43 | 43 | ## Input methods | |
| 44 | 44 | ||
| @@ -38,7 +38,7 @@ bind.cmd+v = spawn cliphist-pick | |||||
| 38 | cliphist list | fuzzel --dmenu | cliphist decode | wl-copy | 38 | cliphist list | fuzzel --dmenu | cliphist decode | wl-copy | ||
| 39 | ``` | 39 | ``` | ||
| 40 | 40 | ||||
| 41 | - | Drag and drop is not supported yet. | 41 | + | Drag and drop works between clients: the surface under the pointer gets the offer, the drop and the chosen action (copy, move or ask, from the modifiers the clients read), and the drag icon follows the cursor. Escape, a refused drop, a dying source or the session lock cancel the drag. |
| 42 | 42 | ||||
| 43 | ## Input methods | 43 | ## Input methods | ||
| 44 | 44 | ||||
M internal/adapters/wayland/clipboard.go +33 -16
| @@ -13,7 +13,9 @@ import ( | |||
| 13 | 13 | // keyboard focus may set them and sees them; clipboard managers such as | |
| 14 | 14 | // cliphist and wl-clipboard use ext_data_control_v1, which sees and sets | |
| 15 | 15 | // both without focus. Like wlroots, any client may bind it. Pasting hands the reader's pipe to the source client: | |
| 16 | - | // the data never goes through neferwl. Drag and drop is not implemented. | |
| 16 | + | // the data never goes through neferwl. Drag and drop (wl_data_device | |
| 17 | + | // start_drag, dnd.go) is a drag core runs and wayland speaks to the source | |
| 18 | + | // and the target; the primary selection and ext_data_control have none. | |
| 17 | 19 | ||
| 18 | 20 | // protected is a defensive DISPLAY admission check, not lock ownership. | |
| 19 | 21 | func (s *Server) protected() bool { | |
| @@ -35,6 +37,11 @@ type clipSource struct { | |||
| 35 | 37 | // used: the source was set, rejected or dragged; it cannot be set | |
| 36 | 38 | // again and takes no more MIME types. | |
| 37 | 39 | used bool | |
| 40 | + | // res is the wl_data_source (nil for the other protocols); actions are | |
| 41 | + | // its set_actions, for drag and drop only (dnd.go). | |
| 42 | + | res *wayland.DataSource | |
| 43 | + | actions uint32 | |
| 44 | + | actionsSet bool | |
| 38 | 45 | } | |
| 39 | 46 | ||
| 40 | 47 | // addMime records a MIME type offered before the source is used. | |
| @@ -175,6 +182,7 @@ func (s *Server) addClipDevice(d *clipDevice) { | |||
| 175 | 182 | ||
| 176 | 183 | // sourceGone clears the selections held by a destroyed source. | |
| 177 | 184 | func (s *Server) sourceGone(src *clipSource) { | |
| 185 | + | s.dndSourceGone(src) | |
| 178 | 186 | for kind := range s.selections { | |
| 179 | 187 | if s.selections[kind] == src { | |
| 180 | 188 | s.selections[kind] = nil | |
| @@ -236,6 +244,7 @@ func (m dataManager) CreateDataSource(r *wayland.DataDeviceManager, id uint32) { | |||
| 236 | 244 | if err != nil { | |
| 237 | 245 | return | |
| 238 | 246 | } | |
| 247 | + | src.res = res | |
| 239 | 248 | src.send = func(mime string, fd int) { res.SendSend(mime, fd) } | |
| 240 | 249 | src.cancel = res.SendCancelled | |
| 241 | 250 | src.alive = res.Resource.Alive | |
| @@ -251,6 +260,7 @@ func (m dataManager) GetDataDevice(r *wayland.DataDeviceManager, id uint32, _ *w | |||
| 251 | 260 | if err != nil { | |
| 252 | 261 | return | |
| 253 | 262 | } | |
| 263 | + | s.addDataDevice(r.Client(), res) | |
| 254 | 264 | s.addClipDevice(&clipDevice{client: r.Client(), kinds: [2]bool{selClipboard: true}, alive: res.Resource.Alive, offer: func(kind int, src *clipSource) { | |
| 255 | 265 | if src == nil { | |
| 256 | 266 | res.SendSelection(nil) | |
| @@ -273,24 +283,26 @@ type dataSource struct{ src *clipSource } | |||
| 273 | 283 | ||
| 274 | 284 | func (d dataSource) Offer(_ *wayland.DataSource, mime string) { d.src.addMime(mime) } | |
| 275 | 285 | func (dataSource) Destroy(*wayland.DataSource) {} | |
| 276 | - | func (dataSource) SetActions(*wayland.DataSource, uint32) {} | |
| 286 | + | ||
| 287 | + | // SetActions records the actions the source allows for a drag: once only, | |
| 288 | + | // before the drag. | |
| 289 | + | func (d dataSource) SetActions(r *wayland.DataSource, actions uint32) { | |
| 290 | + | switch { | |
| 291 | + | case actions&^dndAllActions != 0: | |
| 292 | + | r.PostError(uint32(wayland.DataSourceErrorInvalidActionMask), "invalid action mask") | |
| 293 | + | case d.src.actionsSet || d.src.used: | |
| 294 | + | r.PostError(uint32(wayland.DataSourceErrorInvalidSource), "actions set twice or on a used source") | |
| 295 | + | default: | |
| 296 | + | d.src.actions, d.src.actionsSet = actions, true | |
| 297 | + | } | |
| 298 | + | } | |
| 277 | 299 | ||
| 278 | 300 | type dataDevice struct{ server *Server } | |
| 279 | 301 | ||
| 280 | - | // StartDrag is not implemented: the drag is cancelled. | |
| 281 | - | func (d dataDevice) StartDrag(_ *wayland.DataDevice, src *wayland.DataSource, _, _ *wayland.Surface, _ uint32) { | |
| 282 | - | if src == nil { | |
| 283 | - | return | |
| 284 | - | } | |
| 285 | - | cs := d.server.dataSources[src.Resource] | |
| 286 | - | if cs == nil || cs.used { | |
| 287 | - | // A used source is the selection or already cancelled. | |
| 288 | - | return | |
| 289 | - | } | |
| 290 | - | cs.used = true | |
| 291 | - | if src.Resource.Alive() { | |
| 292 | - | src.SendCancelled() | |
| 293 | - | } | |
| 302 | + | // StartDrag starts a drag answering the client's current press (dnd.go); | |
| 303 | + | // anything else cancels the source. | |
| 304 | + | func (d dataDevice) StartDrag(r *wayland.DataDevice, src *wayland.DataSource, origin, icon *wayland.Surface, serial uint32) { | |
| 305 | + | d.server.startDrag(r, src, origin, icon, serial) | |
| 294 | 306 | } | |
| 295 | 307 | func (d dataDevice) SetSelection(r *wayland.DataDevice, res *wayland.DataSource, _ uint32) { | |
| 296 | 308 | s := d.server | |
| @@ -300,6 +312,11 @@ func (d dataDevice) SetSelection(r *wayland.DataDevice, res *wayland.DataSource, | |||
| 300 | 312 | if src == nil { | |
| 301 | 313 | return | |
| 302 | 314 | } | |
| 315 | + | if src.actionsSet { | |
| 316 | + | // Actions make a source drag-and-drop only. | |
| 317 | + | res.PostError(uint32(wayland.DataSourceErrorInvalidSource), "source with dnd actions used as selection") | |
| 318 | + | return | |
| 319 | + | } | |
| 303 | 320 | } | |
| 304 | 321 | s.setFocused(r.Client(), selClipboard, src) | |
| 305 | 322 | } | |
| @@ -13,7 +13,9 @@ import ( | |||||
| 13 | // keyboard focus may set them and sees them; clipboard managers such as | 13 | // keyboard focus may set them and sees them; clipboard managers such as | ||
| 14 | // cliphist and wl-clipboard use ext_data_control_v1, which sees and sets | 14 | // cliphist and wl-clipboard use ext_data_control_v1, which sees and sets | ||
| 15 | // both without focus. Like wlroots, any client may bind it. Pasting hands the reader's pipe to the source client: | 15 | // both without focus. Like wlroots, any client may bind it. Pasting hands the reader's pipe to the source client: | ||
| 16 | - | // the data never goes through neferwl. Drag and drop is not implemented. | 16 | + | // the data never goes through neferwl. Drag and drop (wl_data_device |
| 17 | + | // start_drag, dnd.go) is a drag core runs and wayland speaks to the source | |||
| 18 | + | // and the target; the primary selection and ext_data_control have none. | |||
| 17 | 19 | ||||
| 18 | // protected is a defensive DISPLAY admission check, not lock ownership. | 20 | // protected is a defensive DISPLAY admission check, not lock ownership. | ||
| 19 | func (s *Server) protected() bool { | 21 | func (s *Server) protected() bool { | ||
| @@ -35,6 +37,11 @@ type clipSource struct { | |||||
| 35 | // used: the source was set, rejected or dragged; it cannot be set | 37 | // used: the source was set, rejected or dragged; it cannot be set | ||
| 36 | // again and takes no more MIME types. | 38 | // again and takes no more MIME types. | ||
| 37 | used bool | 39 | used bool | ||
| 40 | + | // res is the wl_data_source (nil for the other protocols); actions are | |||
| 41 | + | // its set_actions, for drag and drop only (dnd.go). | |||
| 42 | + | res *wayland.DataSource | |||
| 43 | + | actions uint32 | |||
| 44 | + | actionsSet bool | |||
| 38 | } | 45 | } | ||
| 39 | 46 | ||||
| 40 | // addMime records a MIME type offered before the source is used. | 47 | // addMime records a MIME type offered before the source is used. | ||
| @@ -175,6 +182,7 @@ func (s *Server) addClipDevice(d *clipDevice) { | |||||
| 175 | 182 | ||||
| 176 | // sourceGone clears the selections held by a destroyed source. | 183 | // sourceGone clears the selections held by a destroyed source. | ||
| 177 | func (s *Server) sourceGone(src *clipSource) { | 184 | func (s *Server) sourceGone(src *clipSource) { | ||
| 185 | + | s.dndSourceGone(src) | |||
| 178 | for kind := range s.selections { | 186 | for kind := range s.selections { | ||
| 179 | if s.selections[kind] == src { | 187 | if s.selections[kind] == src { | ||
| 180 | s.selections[kind] = nil | 188 | s.selections[kind] = nil | ||
| @@ -236,6 +244,7 @@ func (m dataManager) CreateDataSource(r *wayland.DataDeviceManager, id uint32) { | |||||
| 236 | if err != nil { | 244 | if err != nil { | ||
| 237 | return | 245 | return | ||
| 238 | } | 246 | } | ||
| 247 | + | src.res = res | |||
| 239 | src.send = func(mime string, fd int) { res.SendSend(mime, fd) } | 248 | src.send = func(mime string, fd int) { res.SendSend(mime, fd) } | ||
| 240 | src.cancel = res.SendCancelled | 249 | src.cancel = res.SendCancelled | ||
| 241 | src.alive = res.Resource.Alive | 250 | src.alive = res.Resource.Alive | ||
| @@ -251,6 +260,7 @@ func (m dataManager) GetDataDevice(r *wayland.DataDeviceManager, id uint32, _ *w | |||||
| 251 | if err != nil { | 260 | if err != nil { | ||
| 252 | return | 261 | return | ||
| 253 | } | 262 | } | ||
| 263 | + | s.addDataDevice(r.Client(), res) | |||
| 254 | s.addClipDevice(&clipDevice{client: r.Client(), kinds: [2]bool{selClipboard: true}, alive: res.Resource.Alive, offer: func(kind int, src *clipSource) { | 264 | s.addClipDevice(&clipDevice{client: r.Client(), kinds: [2]bool{selClipboard: true}, alive: res.Resource.Alive, offer: func(kind int, src *clipSource) { | ||
| 255 | if src == nil { | 265 | if src == nil { | ||
| 256 | res.SendSelection(nil) | 266 | res.SendSelection(nil) | ||
| @@ -273,24 +283,26 @@ type dataSource struct{ src *clipSource } | |||||
| 273 | 283 | ||||
| 274 | func (d dataSource) Offer(_ *wayland.DataSource, mime string) { d.src.addMime(mime) } | 284 | func (d dataSource) Offer(_ *wayland.DataSource, mime string) { d.src.addMime(mime) } | ||
| 275 | func (dataSource) Destroy(*wayland.DataSource) {} | 285 | func (dataSource) Destroy(*wayland.DataSource) {} | ||
| 276 | - | func (dataSource) SetActions(*wayland.DataSource, uint32) {} | 286 | + | |
| 287 | + | // SetActions records the actions the source allows for a drag: once only, | |||
| 288 | + | // before the drag. | |||
| 289 | + | func (d dataSource) SetActions(r *wayland.DataSource, actions uint32) { | |||
| 290 | + | switch { | |||
| 291 | + | case actions&^dndAllActions != 0: | |||
| 292 | + | r.PostError(uint32(wayland.DataSourceErrorInvalidActionMask), "invalid action mask") | |||
| 293 | + | case d.src.actionsSet || d.src.used: | |||
| 294 | + | r.PostError(uint32(wayland.DataSourceErrorInvalidSource), "actions set twice or on a used source") | |||
| 295 | + | default: | |||
| 296 | + | d.src.actions, d.src.actionsSet = actions, true | |||
| 297 | + | } | |||
| 298 | + | } | |||
| 277 | 299 | ||||
| 278 | type dataDevice struct{ server *Server } | 300 | type dataDevice struct{ server *Server } | ||
| 279 | 301 | ||||
| 280 | - | // StartDrag is not implemented: the drag is cancelled. | 302 | + | // StartDrag starts a drag answering the client's current press (dnd.go); |
| 281 | - | func (d dataDevice) StartDrag(_ *wayland.DataDevice, src *wayland.DataSource, _, _ *wayland.Surface, _ uint32) { | 303 | + | // anything else cancels the source. |
| 282 | - | if src == nil { | 304 | + | func (d dataDevice) StartDrag(r *wayland.DataDevice, src *wayland.DataSource, origin, icon *wayland.Surface, serial uint32) { |
| 283 | - | return | 305 | + | d.server.startDrag(r, src, origin, icon, serial) |
| 284 | - | } | |||
| 285 | - | cs := d.server.dataSources[src.Resource] | |||
| 286 | - | if cs == nil || cs.used { | |||
| 287 | - | // A used source is the selection or already cancelled. | |||
| 288 | - | return | |||
| 289 | - | } | |||
| 290 | - | cs.used = true | |||
| 291 | - | if src.Resource.Alive() { | |||
| 292 | - | src.SendCancelled() | |||
| 293 | - | } | |||
| 294 | } | 306 | } | ||
| 295 | func (d dataDevice) SetSelection(r *wayland.DataDevice, res *wayland.DataSource, _ uint32) { | 307 | func (d dataDevice) SetSelection(r *wayland.DataDevice, res *wayland.DataSource, _ uint32) { | ||
| 296 | s := d.server | 308 | s := d.server | ||
| @@ -300,6 +312,11 @@ func (d dataDevice) SetSelection(r *wayland.DataDevice, res *wayland.DataSource, | |||||
| 300 | if src == nil { | 312 | if src == nil { | ||
| 301 | return | 313 | return | ||
| 302 | } | 314 | } | ||
| 315 | + | if src.actionsSet { | |||
| 316 | + | // Actions make a source drag-and-drop only. | |||
| 317 | + | res.PostError(uint32(wayland.DataSourceErrorInvalidSource), "source with dnd actions used as selection") | |||
| 318 | + | return | |||
| 319 | + | } | |||
| 303 | } | 320 | } | ||
| 304 | s.setFocused(r.Client(), selClipboard, src) | 321 | s.setFocused(r.Client(), selClipboard, src) | ||
| 305 | } | 322 | } | ||
A internal/adapters/wayland/dnd.go +537 -0
| @@ -0,0 +1,537 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "slices" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 8 | + | "github.com/bnema/purego-libwayland/server" | |
| 9 | + | "golang.org/x/sys/unix" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // Drag and drop (wl_data_device.start_drag). A client starts a drag with the | |
| 13 | + | // serial of its current press; wayland validates it and tells core | |
| 14 | + | // (ports.DataDragStarted). Core owns the pointer from then on: it names the | |
| 15 | + | // surface under the cursor (DataDragFocus, DataDragMotion), the release | |
| 16 | + | // (DataDragDrop) and the Escape key (DataDragCancel). Wayland speaks the | |
| 17 | + | // data device protocol to the source and the target: enter, motion, leave | |
| 18 | + | // and drop with an offer, and the action negotiation the wlroots way. The | |
| 19 | + | // icon is a client surface with its own window ID; core draws it at the | |
| 20 | + | // cursor from the geometry sent with DataDragIcon. | |
| 21 | + | // | |
| 22 | + | // Core ends a drag with DataDragDrop or DataDragCancel and is not told | |
| 23 | + | // (the lock resets both sides). DataDragEnded tells core about an end it | |
| 24 | + | // did not cause: the source or the origin client died. | |
| 25 | + | ||
| 26 | + | // dndAllActions is every wl_data_device_manager.dnd_action bit. | |
| 27 | + | const dndAllActions = uint32(wayland.DataDeviceManagerDndActionCopy | wayland.DataDeviceManagerDndActionMove | wayland.DataDeviceManagerDndActionAsk) | |
| 28 | + | ||
| 29 | + | // dndState is the drag in progress. | |
| 30 | + | type dndState struct { | |
| 31 | + | // src is the source; nil for a drag internal to the origin client. | |
| 32 | + | src *clipSource | |
| 33 | + | client server.Client | |
| 34 | + | // icon is the icon surface (nil: none or destroyed); iconID is the | |
| 35 | + | // window ID core knows it by (0: none). | |
| 36 | + | icon *surface | |
| 37 | + | iconID ports.WindowID | |
| 38 | + | // iconX, iconY is the icon's top left corner relative to the cursor: | |
| 39 | + | // the offsets of its commits add up. iconSent is the last geometry sent. | |
| 40 | + | iconX, iconY int | |
| 41 | + | iconSent ports.DataDragIcon | |
| 42 | + | target *dndTarget | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | const ( | |
| 46 | + | targetActive = iota | |
| 47 | + | targetDropped | |
| 48 | + | targetDone | |
| 49 | + | ) | |
| 50 | + | ||
| 51 | + | // dndTarget is the surface a drag entered. Its offers (one per data device | |
| 52 | + | // of the client) share the negotiation. After the drop it lives on until | |
| 53 | + | // the target finishes or destroys its offers. | |
| 54 | + | type dndTarget struct { | |
| 55 | + | d *dndState | |
| 56 | + | surface *wayland.Surface | |
| 57 | + | devices []*wayland.DataDevice | |
| 58 | + | offers []*dndOffer | |
| 59 | + | // accepted is the MIME type the target accepted; action the action | |
| 60 | + | // chosen for the source. | |
| 61 | + | accepted string | |
| 62 | + | action uint32 | |
| 63 | + | state int | |
| 64 | + | // legacy: no device speaks wl_data_offer v3, so the accepted MIME type | |
| 65 | + | // alone allows the drop and nothing finishes it. | |
| 66 | + | legacy bool | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | // dndOffer is a wl_data_offer of a drag. | |
| 70 | + | type dndOffer struct { | |
| 71 | + | server *Server | |
| 72 | + | t *dndTarget | |
| 73 | + | res *wayland.DataOffer | |
| 74 | + | // actions and preferred are the target's set_actions; sent is the last | |
| 75 | + | // action told to this offer. | |
| 76 | + | actions, preferred, sent uint32 | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // srcActions is what the source allows; a source that never said (a | |
| 80 | + | // pre-v3 client) only copies. | |
| 81 | + | func (src *clipSource) srcActions() uint32 { | |
| 82 | + | if src.actionsSet { | |
| 83 | + | return src.actions | |
| 84 | + | } | |
| 85 | + | return uint32(wayland.DataDeviceManagerDndActionCopy) | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | // chooseAction picks the action like wlroots: the preferred one if both | |
| 89 | + | // allow it, else the first of copy, move, ask. | |
| 90 | + | func chooseAction(avail, preferred uint32) uint32 { | |
| 91 | + | if avail == 0 { | |
| 92 | + | return 0 | |
| 93 | + | } | |
| 94 | + | if preferred&avail != 0 { | |
| 95 | + | return preferred | |
| 96 | + | } | |
| 97 | + | for _, a := range [...]uint32{uint32(wayland.DataDeviceManagerDndActionCopy), uint32(wayland.DataDeviceManagerDndActionMove), uint32(wayland.DataDeviceManagerDndActionAsk)} { | |
| 98 | + | if avail&a != 0 { | |
| 99 | + | return a | |
| 100 | + | } | |
| 101 | + | } | |
| 102 | + | return 0 | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // addDataDevice tracks a client's data devices, the drag targets. | |
| 106 | + | func (s *Server) addDataDevice(c server.Client, dev *wayland.DataDevice) { | |
| 107 | + | if s.dataDevices == nil { | |
| 108 | + | s.dataDevices = map[server.Client][]*wayland.DataDevice{} | |
| 109 | + | } | |
| 110 | + | s.dataDevices[c] = append(s.dataDevices[c], dev) | |
| 111 | + | dev.OnDestroy = func() { | |
| 112 | + | // Copy on write: a drag may be iterating the old list. | |
| 113 | + | list := slices.DeleteFunc(slices.Clone(s.dataDevices[c]), func(x *wayland.DataDevice) bool { return x == dev }) | |
| 114 | + | if len(list) == 0 { | |
| 115 | + | delete(s.dataDevices, c) | |
| 116 | + | // An internal drag has nobody left to tell. | |
| 117 | + | if d := s.dnd; d != nil && d.client == c && d.src == nil { | |
| 118 | + | s.dndEnd(d, false, true) | |
| 119 | + | } | |
| 120 | + | } else { | |
| 121 | + | s.dataDevices[c] = list | |
| 122 | + | } | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | // startDrag handles wl_data_device.start_drag. | |
| 127 | + | func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, origin, icon *wayland.Surface, serial uint32) { | |
| 128 | + | var cs *clipSource | |
| 129 | + | if srcRes != nil { | |
| 130 | + | if cs = s.dataSources[srcRes.Resource]; cs == nil { | |
| 131 | + | return | |
| 132 | + | } | |
| 133 | + | } | |
| 134 | + | var iconSurf *surface | |
| 135 | + | if icon != nil { | |
| 136 | + | if iconSurf = s.surfaces[icon.Resource]; iconSurf == nil { | |
| 137 | + | return | |
| 138 | + | } | |
| 139 | + | if iconSurf.kind != roleNone && iconSurf.kind != roleDragIcon { | |
| 140 | + | r.PostError(uint32(wayland.DataDeviceErrorRole), "icon surface already has another role") | |
| 141 | + | return | |
| 142 | + | } | |
| 143 | + | } | |
| 144 | + | client := r.Client() | |
| 145 | + | valid := !s.protected() && s.dnd == nil && origin != nil && origin.Client() == client && | |
| 146 | + | s.seat.press != 0 && serial == s.seat.press && s.seat.pressClient == client | |
| 147 | + | if cs != nil && (cs.used || !valid) { | |
| 148 | + | // A used source is the selection, a drag source or already | |
| 149 | + | // cancelled: only a source nobody holds is cancelled again. | |
| 150 | + | if !s.sourceBusy(cs) { | |
| 151 | + | cs.used = true | |
| 152 | + | if cs.alive() { | |
| 153 | + | cs.cancel() | |
| 154 | + | } | |
| 155 | + | } | |
| 156 | + | return | |
| 157 | + | } | |
| 158 | + | if !valid { | |
| 159 | + | return | |
| 160 | + | } | |
| 161 | + | d := &dndState{src: cs, client: client, icon: iconSurf} | |
| 162 | + | if cs != nil { | |
| 163 | + | cs.used = true | |
| 164 | + | } | |
| 165 | + | if iconSurf != nil { | |
| 166 | + | d.iconID = s.nextWindow | |
| 167 | + | s.nextWindow++ | |
| 168 | + | iconSurf.kind = roleDragIcon | |
| 169 | + | iconSurf.role = func(bool) { s.dndIconRole(d, iconSurf) } | |
| 170 | + | } | |
| 171 | + | s.dnd = d | |
| 172 | + | s.log.Info().Uint64("icon", uint64(d.iconID)).Bool("source", cs != nil).Msg("data drag started") | |
| 173 | + | s.emit(ports.DataDragStarted{Icon: d.iconID}) | |
| 174 | + | if iconSurf != nil && iconSurf.has { | |
| 175 | + | // The icon was committed before the drag: show what it holds. | |
| 176 | + | iconSurf.committed = damage{full: true} | |
| 177 | + | iconSurf.redraw() | |
| 178 | + | s.dndIconSend(d) | |
| 179 | + | } | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | // sourceBusy reports whether src is the selection, the source of the drag | |
| 183 | + | // or of a drop waiting for its finish. | |
| 184 | + | func (s *Server) sourceBusy(src *clipSource) bool { | |
| 185 | + | if s.selections[selClipboard] == src || s.dnd != nil && s.dnd.src == src { | |
| 186 | + | return true | |
| 187 | + | } | |
| 188 | + | return slices.ContainsFunc(s.dndDrops, func(t *dndTarget) bool { return t.d.src == src }) | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | // dndIconRole runs on each commit of the icon surface, and on its | |
| 192 | + | // destruction. | |
| 193 | + | func (s *Server) dndIconRole(d *dndState, icon *surface) { | |
| 194 | + | if !icon.destroyed || d.icon != icon { | |
| 195 | + | return | |
| 196 | + | } | |
| 197 | + | // The drag goes on without icon. | |
| 198 | + | id := d.iconID | |
| 199 | + | d.icon, d.iconID = nil, 0 | |
| 200 | + | s.emit(ports.DataDragIcon{ID: id}) | |
| 201 | + | s.emitContent(ports.SurfaceContent{ID: id}, damage{full: true}) | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | // dndIconCommit follows a commit of the active icon: its offsets add up | |
| 205 | + | // and its geometry goes to core. | |
| 206 | + | func (s *Server) dndIconCommit(icon *surface, dx, dy int32) { | |
| 207 | + | d := s.dnd | |
| 208 | + | if d == nil || d.icon != icon { | |
| 209 | + | return | |
| 210 | + | } | |
| 211 | + | d.iconX += int(dx) | |
| 212 | + | d.iconY += int(dy) | |
| 213 | + | s.dndIconSend(d) | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | func (s *Server) dndIconSend(d *dndState) { | |
| 217 | + | ic := ports.DataDragIcon{ID: d.iconID, X: d.iconX, Y: d.iconY} | |
| 218 | + | if d.icon != nil && d.icon.has { | |
| 219 | + | ic.W, ic.H = d.icon.content.LogicalW, d.icon.content.LogicalH | |
| 220 | + | } | |
| 221 | + | if ic != d.iconSent { | |
| 222 | + | d.iconSent = ic | |
| 223 | + | s.emit(ic) | |
| 224 | + | } | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | // applyDrag applies the commands core sends during a drag. | |
| 228 | + | func (s *Server) applyDrag(cmd ports.ClientCommand) { | |
| 229 | + | d := s.dnd | |
| 230 | + | if d == nil { | |
| 231 | + | return | |
| 232 | + | } | |
| 233 | + | switch c := cmd.(type) { | |
| 234 | + | case ports.DataDragCancel: | |
| 235 | + | s.dndEnd(d, true, false) | |
| 236 | + | case ports.DataDragDrop: | |
| 237 | + | s.dndDrop(d) | |
| 238 | + | case ports.DataDragFocus: | |
| 239 | + | s.dndFocus(d, c) | |
| 240 | + | case ports.DataDragMotion: | |
| 241 | + | if t := d.target; t != nil && !s.protected() { | |
| 242 | + | surf, _, x, y := s.pointerSurface(c.ID, c.X, c.Y) | |
| 243 | + | if surf == nil || surf.Resource != t.surface.Resource { | |
| 244 | + | return | |
| 245 | + | } | |
| 246 | + | for _, dev := range t.devices { | |
| 247 | + | if dev.Resource.Alive() { | |
| 248 | + | dev.SendMotion(wireMsec(c.Time), server.FixedFromFloat(x), server.FixedFromFloat(y)) | |
| 249 | + | } | |
| 250 | + | } | |
| 251 | + | } | |
| 252 | + | } | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | // dndFocus moves the drag to the surface core names: the old target gets | |
| 256 | + | // leave, the new one enter with an offer. | |
| 257 | + | func (s *Server) dndFocus(d *dndState, c ports.DataDragFocus) { | |
| 258 | + | s.dndLeave(d) | |
| 259 | + | if c.ID == 0 || s.protected() { | |
| 260 | + | return | |
| 261 | + | } | |
| 262 | + | surf, _, x, y := s.pointerSurface(c.ID, c.X, c.Y) | |
| 263 | + | if surf == nil { | |
| 264 | + | return | |
| 265 | + | } | |
| 266 | + | client := surf.Client() | |
| 267 | + | // Without a source the drag is internal to the origin client. | |
| 268 | + | if d.src == nil && client != d.client { | |
| 269 | + | return | |
| 270 | + | } | |
| 271 | + | devices := s.dataDevices[client] | |
| 272 | + | if len(devices) == 0 { | |
| 273 | + | return | |
| 274 | + | } | |
| 275 | + | t := &dndTarget{d: d, surface: surf, devices: devices, legacy: true} | |
| 276 | + | d.target = t | |
| 277 | + | for _, dev := range devices { | |
| 278 | + | if !dev.Resource.Alive() { | |
| 279 | + | continue | |
| 280 | + | } | |
| 281 | + | if dev.Version() >= 3 { | |
| 282 | + | t.legacy = false | |
| 283 | + | } | |
| 284 | + | var offer *wayland.DataOffer | |
| 285 | + | if d.src != nil { | |
| 286 | + | o := &dndOffer{server: s, t: t} | |
| 287 | + | res, err := wayland.NewDataOffer(dev.Client(), dev.Version(), 0, o) | |
| 288 | + | if err != nil { | |
| 289 | + | continue | |
| 290 | + | } | |
| 291 | + | o.res = res | |
| 292 | + | res.OnDestroy = func() { s.dndOfferGone(o) } | |
| 293 | + | t.offers = append(t.offers, o) | |
| 294 | + | dev.SendDataOffer(res) | |
| 295 | + | for _, mime := range d.src.mimes { | |
| 296 | + | res.SendOffer(mime) | |
| 297 | + | } | |
| 298 | + | if dev.Version() >= 3 { | |
| 299 | + | res.SendSourceActions(d.src.srcActions()) | |
| 300 | + | } | |
| 301 | + | offer = res | |
| 302 | + | } | |
| 303 | + | s.serial++ | |
| 304 | + | dev.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y), offer) | |
| 305 | + | } | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | // dndLeave sends leave to the target of the drag and forgets it: its | |
| 309 | + | // offers become inert. | |
| 310 | + | func (s *Server) dndLeave(d *dndState) { | |
| 311 | + | t := d.target | |
| 312 | + | if t == nil { | |
| 313 | + | return | |
| 314 | + | } | |
| 315 | + | d.target = nil | |
| 316 | + | t.state = targetDone | |
| 317 | + | for _, dev := range t.devices { | |
| 318 | + | if dev.Resource.Alive() { | |
| 319 | + | dev.SendLeave() | |
| 320 | + | } | |
| 321 | + | } | |
| 322 | + | s.dndResetSource(t) | |
| 323 | + | } | |
| 324 | + | ||
| 325 | + | // dndResetSource tells the source nothing accepts or acts any more. | |
| 326 | + | func (s *Server) dndResetSource(t *dndTarget) { | |
| 327 | + | src := t.d.src | |
| 328 | + | if src == nil || !src.alive() { | |
| 329 | + | return | |
| 330 | + | } | |
| 331 | + | if t.accepted != "" { | |
| 332 | + | src.res.SendTarget("") | |
| 333 | + | } | |
| 334 | + | if t.action != 0 && src.res.Version() >= 3 { | |
| 335 | + | src.res.SendAction(0) | |
| 336 | + | } | |
| 337 | + | t.accepted, t.action = "", 0 | |
| 338 | + | } | |
| 339 | + | ||
| 340 | + | // dndDrop handles the release of the drag button: a target that accepted | |
| 341 | + | // and agreed on an action gets drop; else the drag is cancelled. | |
| 342 | + | func (s *Server) dndDrop(d *dndState) { | |
| 343 | + | t := d.target | |
| 344 | + | switch { | |
| 345 | + | case t == nil: | |
| 346 | + | s.dndEnd(d, true, false) | |
| 347 | + | case d.src == nil: | |
| 348 | + | // Internal drag: the origin client decides alone. | |
| 349 | + | d.target = nil | |
| 350 | + | t.state = targetDone | |
| 351 | + | s.dndSendDrop(t) | |
| 352 | + | s.dndEnd(d, false, false) | |
| 353 | + | case t.accepted == "" || t.action == 0 && !t.legacy: | |
| 354 | + | s.dndEnd(d, true, false) | |
| 355 | + | default: | |
| 356 | + | d.target = nil | |
| 357 | + | t.state = targetDropped | |
| 358 | + | if t.legacy { | |
| 359 | + | // A pre-v3 target copies and nothing finishes its transfer. | |
| 360 | + | if t.action == 0 { | |
| 361 | + | t.action = uint32(wayland.DataDeviceManagerDndActionCopy) | |
| 362 | + | if d.src.alive() && d.src.res.Version() >= 3 { | |
| 363 | + | d.src.res.SendAction(t.action) | |
| 364 | + | } | |
| 365 | + | } | |
| 366 | + | } else { | |
| 367 | + | s.dndDrops = append(s.dndDrops, t) | |
| 368 | + | } | |
| 369 | + | s.dndSendDrop(t) | |
| 370 | + | if d.src.alive() && d.src.res.Version() >= 3 { | |
| 371 | + | d.src.res.SendDndDropPerformed() | |
| 372 | + | } | |
| 373 | + | s.log.Info().Str("mime", t.accepted).Uint32("action", t.action).Msg("data drag dropped") | |
| 374 | + | s.dndEnd(d, false, false) | |
| 375 | + | } | |
| 376 | + | } | |
| 377 | + | ||
| 378 | + | func (s *Server) dndSendDrop(t *dndTarget) { | |
| 379 | + | for _, dev := range t.devices { | |
| 380 | + | if dev.Resource.Alive() { | |
| 381 | + | dev.SendDrop() | |
| 382 | + | } | |
| 383 | + | } | |
| 384 | + | } | |
| 385 | + | ||
| 386 | + | // dndEnd ends the drag d: the target gets leave, the source cancelled if | |
| 387 | + | // cancel (it is not once dropped), the icon goes. ended tells core about an | |
| 388 | + | // end it did not cause. | |
| 389 | + | func (s *Server) dndEnd(d *dndState, cancel, ended bool) { | |
| 390 | + | s.dndLeave(d) | |
| 391 | + | if cancel && d.src != nil && d.src.alive() { | |
| 392 | + | d.src.cancel() | |
| 393 | + | } | |
| 394 | + | if s.dnd == d { | |
| 395 | + | s.dnd = nil | |
| 396 | + | } | |
| 397 | + | if d.icon != nil { | |
| 398 | + | d.icon.role = nil | |
| 399 | + | d.icon = nil | |
| 400 | + | } | |
| 401 | + | if d.iconID != 0 { | |
| 402 | + | s.emitContent(ports.SurfaceContent{ID: d.iconID}, damage{full: true}) | |
| 403 | + | d.iconID = 0 | |
| 404 | + | } | |
| 405 | + | if ended { | |
| 406 | + | s.log.Info().Msg("data drag ended") | |
| 407 | + | s.emit(ports.DataDragEnded{}) | |
| 408 | + | } | |
| 409 | + | } | |
| 410 | + | ||
| 411 | + | // dndSourceGone ends the drags of a destroyed source. | |
| 412 | + | func (s *Server) dndSourceGone(src *clipSource) { | |
| 413 | + | if d := s.dnd; d != nil && d.src == src { | |
| 414 | + | s.dndEnd(d, false, true) | |
| 415 | + | } | |
| 416 | + | for _, t := range slices.Clone(s.dndDrops) { | |
| 417 | + | if t.d.src == src { | |
| 418 | + | s.dndDropDone(t) | |
| 419 | + | } | |
| 420 | + | } | |
| 421 | + | } | |
| 422 | + | ||
| 423 | + | // dndDropDone forgets a dropped target. | |
| 424 | + | func (s *Server) dndDropDone(t *dndTarget) { | |
| 425 | + | t.state = targetDone | |
| 426 | + | s.dndDrops = slices.DeleteFunc(s.dndDrops, func(x *dndTarget) bool { return x == t }) | |
| 427 | + | } | |
| 428 | + | ||
| 429 | + | // dndSurfaceGone clears the drag target when its surface is destroyed. | |
| 430 | + | func (s *Server) dndSurfaceGone(surf *surface) { | |
| 431 | + | if d := s.dnd; d != nil && d.target != nil && surf.wl != nil && d.target.surface.Resource == surf.wl.Resource { | |
| 432 | + | s.dndLeave(d) | |
| 433 | + | } | |
| 434 | + | } | |
| 435 | + | ||
| 436 | + | // dndCancelAll cancels the drag and the drops waiting for their finish: the | |
| 437 | + | // session is protected. | |
| 438 | + | func (s *Server) dndCancelAll() { | |
| 439 | + | if d := s.dnd; d != nil { | |
| 440 | + | s.dndEnd(d, true, false) | |
| 441 | + | } | |
| 442 | + | for _, t := range slices.Clone(s.dndDrops) { | |
| 443 | + | if src := t.d.src; src != nil && src.alive() { | |
| 444 | + | src.cancel() | |
| 445 | + | } | |
| 446 | + | s.dndDropDone(t) | |
| 447 | + | } | |
| 448 | + | } | |
| 449 | + | ||
| 450 | + | // dndOfferGone runs when an offer of a drag is destroyed: a dropped target | |
| 451 | + | // whose offers are all gone without finish cancels the source. | |
| 452 | + | func (s *Server) dndOfferGone(o *dndOffer) { | |
| 453 | + | t := o.t | |
| 454 | + | t.offers = slices.DeleteFunc(t.offers, func(x *dndOffer) bool { return x == o }) | |
| 455 | + | if t.state == targetDropped && !t.legacy && len(t.offers) == 0 { | |
| 456 | + | if src := t.d.src; src != nil && src.alive() { | |
| 457 | + | src.cancel() | |
| 458 | + | } | |
| 459 | + | s.dndDropDone(t) | |
| 460 | + | } | |
| 461 | + | } | |
| 462 | + | ||
| 463 | + | // update recomputes the action after the target or the source changed | |
| 464 | + | // something, and tells both when it changed. | |
| 465 | + | func (o *dndOffer) update() { | |
| 466 | + | t, src := o.t, o.t.d.src | |
| 467 | + | if t.state == targetDone || src == nil || !src.alive() { | |
| 468 | + | return | |
| 469 | + | } | |
| 470 | + | action := chooseAction(o.actions&src.srcActions(), o.preferred) | |
| 471 | + | if action != t.action { | |
| 472 | + | t.action = action | |
| 473 | + | if src.res.Version() >= 3 { | |
| 474 | + | src.res.SendAction(action) | |
| 475 | + | } | |
| 476 | + | } | |
| 477 | + | if action != o.sent { | |
| 478 | + | o.sent = action | |
| 479 | + | if o.res.Version() >= 3 { | |
| 480 | + | o.res.SendAction(action) | |
| 481 | + | } | |
| 482 | + | } | |
| 483 | + | } | |
| 484 | + | ||
| 485 | + | func (o *dndOffer) Accept(_ *wayland.DataOffer, _ uint32, mime string) { | |
| 486 | + | t := o.t | |
| 487 | + | if t.state != targetActive || !t.d.src.alive() { | |
| 488 | + | return | |
| 489 | + | } | |
| 490 | + | t.accepted = mime | |
| 491 | + | t.d.src.res.SendTarget(mime) | |
| 492 | + | } | |
| 493 | + | ||
| 494 | + | func (o *dndOffer) Receive(_ *wayland.DataOffer, mime string, fd int) { | |
| 495 | + | defer unix.Close(fd) | |
| 496 | + | src := o.t.d.src | |
| 497 | + | if o.server.protected() || o.t.state == targetDone || !src.alive() { | |
| 498 | + | return | |
| 499 | + | } | |
| 500 | + | if slices.Contains(src.mimes, mime) { | |
| 501 | + | src.send(mime, fd) | |
| 502 | + | } | |
| 503 | + | } | |
| 504 | + | ||
| 505 | + | func (*dndOffer) Destroy(*wayland.DataOffer) {} | |
| 506 | + | ||
| 507 | + | func (o *dndOffer) Finish(r *wayland.DataOffer) { | |
| 508 | + | t := o.t | |
| 509 | + | switch t.state { | |
| 510 | + | case targetDone: | |
| 511 | + | return | |
| 512 | + | case targetActive: | |
| 513 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish before the drop") | |
| 514 | + | return | |
| 515 | + | } | |
| 516 | + | if t.action == 0 || t.action == uint32(wayland.DataDeviceManagerDndActionAsk) { | |
| 517 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without a final action") | |
| 518 | + | return | |
| 519 | + | } | |
| 520 | + | o.server.dndDropDone(t) | |
| 521 | + | if src := t.d.src; src.alive() && src.res.Version() >= 3 { | |
| 522 | + | src.res.SendDndFinished() | |
| 523 | + | } | |
| 524 | + | } | |
| 525 | + | ||
| 526 | + | func (o *dndOffer) SetActions(r *wayland.DataOffer, actions, preferred uint32) { | |
| 527 | + | if actions&^dndAllActions != 0 { | |
| 528 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidActionMask), "invalid action mask") | |
| 529 | + | return | |
| 530 | + | } | |
| 531 | + | if preferred != 0 && (preferred&(preferred-1) != 0 || preferred&actions == 0) { | |
| 532 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "invalid preferred action") | |
| 533 | + | return | |
| 534 | + | } | |
| 535 | + | o.actions, o.preferred = actions, preferred | |
| 536 | + | o.update() | |
| 537 | + | } | |
| @@ -0,0 +1,537 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | "github.com/bnema/purego-libwayland/server" | |||
| 9 | + | "golang.org/x/sys/unix" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // Drag and drop (wl_data_device.start_drag). A client starts a drag with the | |||
| 13 | + | // serial of its current press; wayland validates it and tells core | |||
| 14 | + | // (ports.DataDragStarted). Core owns the pointer from then on: it names the | |||
| 15 | + | // surface under the cursor (DataDragFocus, DataDragMotion), the release | |||
| 16 | + | // (DataDragDrop) and the Escape key (DataDragCancel). Wayland speaks the | |||
| 17 | + | // data device protocol to the source and the target: enter, motion, leave | |||
| 18 | + | // and drop with an offer, and the action negotiation the wlroots way. The | |||
| 19 | + | // icon is a client surface with its own window ID; core draws it at the | |||
| 20 | + | // cursor from the geometry sent with DataDragIcon. | |||
| 21 | + | // | |||
| 22 | + | // Core ends a drag with DataDragDrop or DataDragCancel and is not told | |||
| 23 | + | // (the lock resets both sides). DataDragEnded tells core about an end it | |||
| 24 | + | // did not cause: the source or the origin client died. | |||
| 25 | + | ||||
| 26 | + | // dndAllActions is every wl_data_device_manager.dnd_action bit. | |||
| 27 | + | const dndAllActions = uint32(wayland.DataDeviceManagerDndActionCopy | wayland.DataDeviceManagerDndActionMove | wayland.DataDeviceManagerDndActionAsk) | |||
| 28 | + | ||||
| 29 | + | // dndState is the drag in progress. | |||
| 30 | + | type dndState struct { | |||
| 31 | + | // src is the source; nil for a drag internal to the origin client. | |||
| 32 | + | src *clipSource | |||
| 33 | + | client server.Client | |||
| 34 | + | // icon is the icon surface (nil: none or destroyed); iconID is the | |||
| 35 | + | // window ID core knows it by (0: none). | |||
| 36 | + | icon *surface | |||
| 37 | + | iconID ports.WindowID | |||
| 38 | + | // iconX, iconY is the icon's top left corner relative to the cursor: | |||
| 39 | + | // the offsets of its commits add up. iconSent is the last geometry sent. | |||
| 40 | + | iconX, iconY int | |||
| 41 | + | iconSent ports.DataDragIcon | |||
| 42 | + | target *dndTarget | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | const ( | |||
| 46 | + | targetActive = iota | |||
| 47 | + | targetDropped | |||
| 48 | + | targetDone | |||
| 49 | + | ) | |||
| 50 | + | ||||
| 51 | + | // dndTarget is the surface a drag entered. Its offers (one per data device | |||
| 52 | + | // of the client) share the negotiation. After the drop it lives on until | |||
| 53 | + | // the target finishes or destroys its offers. | |||
| 54 | + | type dndTarget struct { | |||
| 55 | + | d *dndState | |||
| 56 | + | surface *wayland.Surface | |||
| 57 | + | devices []*wayland.DataDevice | |||
| 58 | + | offers []*dndOffer | |||
| 59 | + | // accepted is the MIME type the target accepted; action the action | |||
| 60 | + | // chosen for the source. | |||
| 61 | + | accepted string | |||
| 62 | + | action uint32 | |||
| 63 | + | state int | |||
| 64 | + | // legacy: no device speaks wl_data_offer v3, so the accepted MIME type | |||
| 65 | + | // alone allows the drop and nothing finishes it. | |||
| 66 | + | legacy bool | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | // dndOffer is a wl_data_offer of a drag. | |||
| 70 | + | type dndOffer struct { | |||
| 71 | + | server *Server | |||
| 72 | + | t *dndTarget | |||
| 73 | + | res *wayland.DataOffer | |||
| 74 | + | // actions and preferred are the target's set_actions; sent is the last | |||
| 75 | + | // action told to this offer. | |||
| 76 | + | actions, preferred, sent uint32 | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | // srcActions is what the source allows; a source that never said (a | |||
| 80 | + | // pre-v3 client) only copies. | |||
| 81 | + | func (src *clipSource) srcActions() uint32 { | |||
| 82 | + | if src.actionsSet { | |||
| 83 | + | return src.actions | |||
| 84 | + | } | |||
| 85 | + | return uint32(wayland.DataDeviceManagerDndActionCopy) | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | // chooseAction picks the action like wlroots: the preferred one if both | |||
| 89 | + | // allow it, else the first of copy, move, ask. | |||
| 90 | + | func chooseAction(avail, preferred uint32) uint32 { | |||
| 91 | + | if avail == 0 { | |||
| 92 | + | return 0 | |||
| 93 | + | } | |||
| 94 | + | if preferred&avail != 0 { | |||
| 95 | + | return preferred | |||
| 96 | + | } | |||
| 97 | + | for _, a := range [...]uint32{uint32(wayland.DataDeviceManagerDndActionCopy), uint32(wayland.DataDeviceManagerDndActionMove), uint32(wayland.DataDeviceManagerDndActionAsk)} { | |||
| 98 | + | if avail&a != 0 { | |||
| 99 | + | return a | |||
| 100 | + | } | |||
| 101 | + | } | |||
| 102 | + | return 0 | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // addDataDevice tracks a client's data devices, the drag targets. | |||
| 106 | + | func (s *Server) addDataDevice(c server.Client, dev *wayland.DataDevice) { | |||
| 107 | + | if s.dataDevices == nil { | |||
| 108 | + | s.dataDevices = map[server.Client][]*wayland.DataDevice{} | |||
| 109 | + | } | |||
| 110 | + | s.dataDevices[c] = append(s.dataDevices[c], dev) | |||
| 111 | + | dev.OnDestroy = func() { | |||
| 112 | + | // Copy on write: a drag may be iterating the old list. | |||
| 113 | + | list := slices.DeleteFunc(slices.Clone(s.dataDevices[c]), func(x *wayland.DataDevice) bool { return x == dev }) | |||
| 114 | + | if len(list) == 0 { | |||
| 115 | + | delete(s.dataDevices, c) | |||
| 116 | + | // An internal drag has nobody left to tell. | |||
| 117 | + | if d := s.dnd; d != nil && d.client == c && d.src == nil { | |||
| 118 | + | s.dndEnd(d, false, true) | |||
| 119 | + | } | |||
| 120 | + | } else { | |||
| 121 | + | s.dataDevices[c] = list | |||
| 122 | + | } | |||
| 123 | + | } | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | // startDrag handles wl_data_device.start_drag. | |||
| 127 | + | func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, origin, icon *wayland.Surface, serial uint32) { | |||
| 128 | + | var cs *clipSource | |||
| 129 | + | if srcRes != nil { | |||
| 130 | + | if cs = s.dataSources[srcRes.Resource]; cs == nil { | |||
| 131 | + | return | |||
| 132 | + | } | |||
| 133 | + | } | |||
| 134 | + | var iconSurf *surface | |||
| 135 | + | if icon != nil { | |||
| 136 | + | if iconSurf = s.surfaces[icon.Resource]; iconSurf == nil { | |||
| 137 | + | return | |||
| 138 | + | } | |||
| 139 | + | if iconSurf.kind != roleNone && iconSurf.kind != roleDragIcon { | |||
| 140 | + | r.PostError(uint32(wayland.DataDeviceErrorRole), "icon surface already has another role") | |||
| 141 | + | return | |||
| 142 | + | } | |||
| 143 | + | } | |||
| 144 | + | client := r.Client() | |||
| 145 | + | valid := !s.protected() && s.dnd == nil && origin != nil && origin.Client() == client && | |||
| 146 | + | s.seat.press != 0 && serial == s.seat.press && s.seat.pressClient == client | |||
| 147 | + | if cs != nil && (cs.used || !valid) { | |||
| 148 | + | // A used source is the selection, a drag source or already | |||
| 149 | + | // cancelled: only a source nobody holds is cancelled again. | |||
| 150 | + | if !s.sourceBusy(cs) { | |||
| 151 | + | cs.used = true | |||
| 152 | + | if cs.alive() { | |||
| 153 | + | cs.cancel() | |||
| 154 | + | } | |||
| 155 | + | } | |||
| 156 | + | return | |||
| 157 | + | } | |||
| 158 | + | if !valid { | |||
| 159 | + | return | |||
| 160 | + | } | |||
| 161 | + | d := &dndState{src: cs, client: client, icon: iconSurf} | |||
| 162 | + | if cs != nil { | |||
| 163 | + | cs.used = true | |||
| 164 | + | } | |||
| 165 | + | if iconSurf != nil { | |||
| 166 | + | d.iconID = s.nextWindow | |||
| 167 | + | s.nextWindow++ | |||
| 168 | + | iconSurf.kind = roleDragIcon | |||
| 169 | + | iconSurf.role = func(bool) { s.dndIconRole(d, iconSurf) } | |||
| 170 | + | } | |||
| 171 | + | s.dnd = d | |||
| 172 | + | s.log.Info().Uint64("icon", uint64(d.iconID)).Bool("source", cs != nil).Msg("data drag started") | |||
| 173 | + | s.emit(ports.DataDragStarted{Icon: d.iconID}) | |||
| 174 | + | if iconSurf != nil && iconSurf.has { | |||
| 175 | + | // The icon was committed before the drag: show what it holds. | |||
| 176 | + | iconSurf.committed = damage{full: true} | |||
| 177 | + | iconSurf.redraw() | |||
| 178 | + | s.dndIconSend(d) | |||
| 179 | + | } | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | // sourceBusy reports whether src is the selection, the source of the drag | |||
| 183 | + | // or of a drop waiting for its finish. | |||
| 184 | + | func (s *Server) sourceBusy(src *clipSource) bool { | |||
| 185 | + | if s.selections[selClipboard] == src || s.dnd != nil && s.dnd.src == src { | |||
| 186 | + | return true | |||
| 187 | + | } | |||
| 188 | + | return slices.ContainsFunc(s.dndDrops, func(t *dndTarget) bool { return t.d.src == src }) | |||
| 189 | + | } | |||
| 190 | + | ||||
| 191 | + | // dndIconRole runs on each commit of the icon surface, and on its | |||
| 192 | + | // destruction. | |||
| 193 | + | func (s *Server) dndIconRole(d *dndState, icon *surface) { | |||
| 194 | + | if !icon.destroyed || d.icon != icon { | |||
| 195 | + | return | |||
| 196 | + | } | |||
| 197 | + | // The drag goes on without icon. | |||
| 198 | + | id := d.iconID | |||
| 199 | + | d.icon, d.iconID = nil, 0 | |||
| 200 | + | s.emit(ports.DataDragIcon{ID: id}) | |||
| 201 | + | s.emitContent(ports.SurfaceContent{ID: id}, damage{full: true}) | |||
| 202 | + | } | |||
| 203 | + | ||||
| 204 | + | // dndIconCommit follows a commit of the active icon: its offsets add up | |||
| 205 | + | // and its geometry goes to core. | |||
| 206 | + | func (s *Server) dndIconCommit(icon *surface, dx, dy int32) { | |||
| 207 | + | d := s.dnd | |||
| 208 | + | if d == nil || d.icon != icon { | |||
| 209 | + | return | |||
| 210 | + | } | |||
| 211 | + | d.iconX += int(dx) | |||
| 212 | + | d.iconY += int(dy) | |||
| 213 | + | s.dndIconSend(d) | |||
| 214 | + | } | |||
| 215 | + | ||||
| 216 | + | func (s *Server) dndIconSend(d *dndState) { | |||
| 217 | + | ic := ports.DataDragIcon{ID: d.iconID, X: d.iconX, Y: d.iconY} | |||
| 218 | + | if d.icon != nil && d.icon.has { | |||
| 219 | + | ic.W, ic.H = d.icon.content.LogicalW, d.icon.content.LogicalH | |||
| 220 | + | } | |||
| 221 | + | if ic != d.iconSent { | |||
| 222 | + | d.iconSent = ic | |||
| 223 | + | s.emit(ic) | |||
| 224 | + | } | |||
| 225 | + | } | |||
| 226 | + | ||||
| 227 | + | // applyDrag applies the commands core sends during a drag. | |||
| 228 | + | func (s *Server) applyDrag(cmd ports.ClientCommand) { | |||
| 229 | + | d := s.dnd | |||
| 230 | + | if d == nil { | |||
| 231 | + | return | |||
| 232 | + | } | |||
| 233 | + | switch c := cmd.(type) { | |||
| 234 | + | case ports.DataDragCancel: | |||
| 235 | + | s.dndEnd(d, true, false) | |||
| 236 | + | case ports.DataDragDrop: | |||
| 237 | + | s.dndDrop(d) | |||
| 238 | + | case ports.DataDragFocus: | |||
| 239 | + | s.dndFocus(d, c) | |||
| 240 | + | case ports.DataDragMotion: | |||
| 241 | + | if t := d.target; t != nil && !s.protected() { | |||
| 242 | + | surf, _, x, y := s.pointerSurface(c.ID, c.X, c.Y) | |||
| 243 | + | if surf == nil || surf.Resource != t.surface.Resource { | |||
| 244 | + | return | |||
| 245 | + | } | |||
| 246 | + | for _, dev := range t.devices { | |||
| 247 | + | if dev.Resource.Alive() { | |||
| 248 | + | dev.SendMotion(wireMsec(c.Time), server.FixedFromFloat(x), server.FixedFromFloat(y)) | |||
| 249 | + | } | |||
| 250 | + | } | |||
| 251 | + | } | |||
| 252 | + | } | |||
| 253 | + | } | |||
| 254 | + | ||||
| 255 | + | // dndFocus moves the drag to the surface core names: the old target gets | |||
| 256 | + | // leave, the new one enter with an offer. | |||
| 257 | + | func (s *Server) dndFocus(d *dndState, c ports.DataDragFocus) { | |||
| 258 | + | s.dndLeave(d) | |||
| 259 | + | if c.ID == 0 || s.protected() { | |||
| 260 | + | return | |||
| 261 | + | } | |||
| 262 | + | surf, _, x, y := s.pointerSurface(c.ID, c.X, c.Y) | |||
| 263 | + | if surf == nil { | |||
| 264 | + | return | |||
| 265 | + | } | |||
| 266 | + | client := surf.Client() | |||
| 267 | + | // Without a source the drag is internal to the origin client. | |||
| 268 | + | if d.src == nil && client != d.client { | |||
| 269 | + | return | |||
| 270 | + | } | |||
| 271 | + | devices := s.dataDevices[client] | |||
| 272 | + | if len(devices) == 0 { | |||
| 273 | + | return | |||
| 274 | + | } | |||
| 275 | + | t := &dndTarget{d: d, surface: surf, devices: devices, legacy: true} | |||
| 276 | + | d.target = t | |||
| 277 | + | for _, dev := range devices { | |||
| 278 | + | if !dev.Resource.Alive() { | |||
| 279 | + | continue | |||
| 280 | + | } | |||
| 281 | + | if dev.Version() >= 3 { | |||
| 282 | + | t.legacy = false | |||
| 283 | + | } | |||
| 284 | + | var offer *wayland.DataOffer | |||
| 285 | + | if d.src != nil { | |||
| 286 | + | o := &dndOffer{server: s, t: t} | |||
| 287 | + | res, err := wayland.NewDataOffer(dev.Client(), dev.Version(), 0, o) | |||
| 288 | + | if err != nil { | |||
| 289 | + | continue | |||
| 290 | + | } | |||
| 291 | + | o.res = res | |||
| 292 | + | res.OnDestroy = func() { s.dndOfferGone(o) } | |||
| 293 | + | t.offers = append(t.offers, o) | |||
| 294 | + | dev.SendDataOffer(res) | |||
| 295 | + | for _, mime := range d.src.mimes { | |||
| 296 | + | res.SendOffer(mime) | |||
| 297 | + | } | |||
| 298 | + | if dev.Version() >= 3 { | |||
| 299 | + | res.SendSourceActions(d.src.srcActions()) | |||
| 300 | + | } | |||
| 301 | + | offer = res | |||
| 302 | + | } | |||
| 303 | + | s.serial++ | |||
| 304 | + | dev.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y), offer) | |||
| 305 | + | } | |||
| 306 | + | } | |||
| 307 | + | ||||
| 308 | + | // dndLeave sends leave to the target of the drag and forgets it: its | |||
| 309 | + | // offers become inert. | |||
| 310 | + | func (s *Server) dndLeave(d *dndState) { | |||
| 311 | + | t := d.target | |||
| 312 | + | if t == nil { | |||
| 313 | + | return | |||
| 314 | + | } | |||
| 315 | + | d.target = nil | |||
| 316 | + | t.state = targetDone | |||
| 317 | + | for _, dev := range t.devices { | |||
| 318 | + | if dev.Resource.Alive() { | |||
| 319 | + | dev.SendLeave() | |||
| 320 | + | } | |||
| 321 | + | } | |||
| 322 | + | s.dndResetSource(t) | |||
| 323 | + | } | |||
| 324 | + | ||||
| 325 | + | // dndResetSource tells the source nothing accepts or acts any more. | |||
| 326 | + | func (s *Server) dndResetSource(t *dndTarget) { | |||
| 327 | + | src := t.d.src | |||
| 328 | + | if src == nil || !src.alive() { | |||
| 329 | + | return | |||
| 330 | + | } | |||
| 331 | + | if t.accepted != "" { | |||
| 332 | + | src.res.SendTarget("") | |||
| 333 | + | } | |||
| 334 | + | if t.action != 0 && src.res.Version() >= 3 { | |||
| 335 | + | src.res.SendAction(0) | |||
| 336 | + | } | |||
| 337 | + | t.accepted, t.action = "", 0 | |||
| 338 | + | } | |||
| 339 | + | ||||
| 340 | + | // dndDrop handles the release of the drag button: a target that accepted | |||
| 341 | + | // and agreed on an action gets drop; else the drag is cancelled. | |||
| 342 | + | func (s *Server) dndDrop(d *dndState) { | |||
| 343 | + | t := d.target | |||
| 344 | + | switch { | |||
| 345 | + | case t == nil: | |||
| 346 | + | s.dndEnd(d, true, false) | |||
| 347 | + | case d.src == nil: | |||
| 348 | + | // Internal drag: the origin client decides alone. | |||
| 349 | + | d.target = nil | |||
| 350 | + | t.state = targetDone | |||
| 351 | + | s.dndSendDrop(t) | |||
| 352 | + | s.dndEnd(d, false, false) | |||
| 353 | + | case t.accepted == "" || t.action == 0 && !t.legacy: | |||
| 354 | + | s.dndEnd(d, true, false) | |||
| 355 | + | default: | |||
| 356 | + | d.target = nil | |||
| 357 | + | t.state = targetDropped | |||
| 358 | + | if t.legacy { | |||
| 359 | + | // A pre-v3 target copies and nothing finishes its transfer. | |||
| 360 | + | if t.action == 0 { | |||
| 361 | + | t.action = uint32(wayland.DataDeviceManagerDndActionCopy) | |||
| 362 | + | if d.src.alive() && d.src.res.Version() >= 3 { | |||
| 363 | + | d.src.res.SendAction(t.action) | |||
| 364 | + | } | |||
| 365 | + | } | |||
| 366 | + | } else { | |||
| 367 | + | s.dndDrops = append(s.dndDrops, t) | |||
| 368 | + | } | |||
| 369 | + | s.dndSendDrop(t) | |||
| 370 | + | if d.src.alive() && d.src.res.Version() >= 3 { | |||
| 371 | + | d.src.res.SendDndDropPerformed() | |||
| 372 | + | } | |||
| 373 | + | s.log.Info().Str("mime", t.accepted).Uint32("action", t.action).Msg("data drag dropped") | |||
| 374 | + | s.dndEnd(d, false, false) | |||
| 375 | + | } | |||
| 376 | + | } | |||
| 377 | + | ||||
| 378 | + | func (s *Server) dndSendDrop(t *dndTarget) { | |||
| 379 | + | for _, dev := range t.devices { | |||
| 380 | + | if dev.Resource.Alive() { | |||
| 381 | + | dev.SendDrop() | |||
| 382 | + | } | |||
| 383 | + | } | |||
| 384 | + | } | |||
| 385 | + | ||||
| 386 | + | // dndEnd ends the drag d: the target gets leave, the source cancelled if | |||
| 387 | + | // cancel (it is not once dropped), the icon goes. ended tells core about an | |||
| 388 | + | // end it did not cause. | |||
| 389 | + | func (s *Server) dndEnd(d *dndState, cancel, ended bool) { | |||
| 390 | + | s.dndLeave(d) | |||
| 391 | + | if cancel && d.src != nil && d.src.alive() { | |||
| 392 | + | d.src.cancel() | |||
| 393 | + | } | |||
| 394 | + | if s.dnd == d { | |||
| 395 | + | s.dnd = nil | |||
| 396 | + | } | |||
| 397 | + | if d.icon != nil { | |||
| 398 | + | d.icon.role = nil | |||
| 399 | + | d.icon = nil | |||
| 400 | + | } | |||
| 401 | + | if d.iconID != 0 { | |||
| 402 | + | s.emitContent(ports.SurfaceContent{ID: d.iconID}, damage{full: true}) | |||
| 403 | + | d.iconID = 0 | |||
| 404 | + | } | |||
| 405 | + | if ended { | |||
| 406 | + | s.log.Info().Msg("data drag ended") | |||
| 407 | + | s.emit(ports.DataDragEnded{}) | |||
| 408 | + | } | |||
| 409 | + | } | |||
| 410 | + | ||||
| 411 | + | // dndSourceGone ends the drags of a destroyed source. | |||
| 412 | + | func (s *Server) dndSourceGone(src *clipSource) { | |||
| 413 | + | if d := s.dnd; d != nil && d.src == src { | |||
| 414 | + | s.dndEnd(d, false, true) | |||
| 415 | + | } | |||
| 416 | + | for _, t := range slices.Clone(s.dndDrops) { | |||
| 417 | + | if t.d.src == src { | |||
| 418 | + | s.dndDropDone(t) | |||
| 419 | + | } | |||
| 420 | + | } | |||
| 421 | + | } | |||
| 422 | + | ||||
| 423 | + | // dndDropDone forgets a dropped target. | |||
| 424 | + | func (s *Server) dndDropDone(t *dndTarget) { | |||
| 425 | + | t.state = targetDone | |||
| 426 | + | s.dndDrops = slices.DeleteFunc(s.dndDrops, func(x *dndTarget) bool { return x == t }) | |||
| 427 | + | } | |||
| 428 | + | ||||
| 429 | + | // dndSurfaceGone clears the drag target when its surface is destroyed. | |||
| 430 | + | func (s *Server) dndSurfaceGone(surf *surface) { | |||
| 431 | + | if d := s.dnd; d != nil && d.target != nil && surf.wl != nil && d.target.surface.Resource == surf.wl.Resource { | |||
| 432 | + | s.dndLeave(d) | |||
| 433 | + | } | |||
| 434 | + | } | |||
| 435 | + | ||||
| 436 | + | // dndCancelAll cancels the drag and the drops waiting for their finish: the | |||
| 437 | + | // session is protected. | |||
| 438 | + | func (s *Server) dndCancelAll() { | |||
| 439 | + | if d := s.dnd; d != nil { | |||
| 440 | + | s.dndEnd(d, true, false) | |||
| 441 | + | } | |||
| 442 | + | for _, t := range slices.Clone(s.dndDrops) { | |||
| 443 | + | if src := t.d.src; src != nil && src.alive() { | |||
| 444 | + | src.cancel() | |||
| 445 | + | } | |||
| 446 | + | s.dndDropDone(t) | |||
| 447 | + | } | |||
| 448 | + | } | |||
| 449 | + | ||||
| 450 | + | // dndOfferGone runs when an offer of a drag is destroyed: a dropped target | |||
| 451 | + | // whose offers are all gone without finish cancels the source. | |||
| 452 | + | func (s *Server) dndOfferGone(o *dndOffer) { | |||
| 453 | + | t := o.t | |||
| 454 | + | t.offers = slices.DeleteFunc(t.offers, func(x *dndOffer) bool { return x == o }) | |||
| 455 | + | if t.state == targetDropped && !t.legacy && len(t.offers) == 0 { | |||
| 456 | + | if src := t.d.src; src != nil && src.alive() { | |||
| 457 | + | src.cancel() | |||
| 458 | + | } | |||
| 459 | + | s.dndDropDone(t) | |||
| 460 | + | } | |||
| 461 | + | } | |||
| 462 | + | ||||
| 463 | + | // update recomputes the action after the target or the source changed | |||
| 464 | + | // something, and tells both when it changed. | |||
| 465 | + | func (o *dndOffer) update() { | |||
| 466 | + | t, src := o.t, o.t.d.src | |||
| 467 | + | if t.state == targetDone || src == nil || !src.alive() { | |||
| 468 | + | return | |||
| 469 | + | } | |||
| 470 | + | action := chooseAction(o.actions&src.srcActions(), o.preferred) | |||
| 471 | + | if action != t.action { | |||
| 472 | + | t.action = action | |||
| 473 | + | if src.res.Version() >= 3 { | |||
| 474 | + | src.res.SendAction(action) | |||
| 475 | + | } | |||
| 476 | + | } | |||
| 477 | + | if action != o.sent { | |||
| 478 | + | o.sent = action | |||
| 479 | + | if o.res.Version() >= 3 { | |||
| 480 | + | o.res.SendAction(action) | |||
| 481 | + | } | |||
| 482 | + | } | |||
| 483 | + | } | |||
| 484 | + | ||||
| 485 | + | func (o *dndOffer) Accept(_ *wayland.DataOffer, _ uint32, mime string) { | |||
| 486 | + | t := o.t | |||
| 487 | + | if t.state != targetActive || !t.d.src.alive() { | |||
| 488 | + | return | |||
| 489 | + | } | |||
| 490 | + | t.accepted = mime | |||
| 491 | + | t.d.src.res.SendTarget(mime) | |||
| 492 | + | } | |||
| 493 | + | ||||
| 494 | + | func (o *dndOffer) Receive(_ *wayland.DataOffer, mime string, fd int) { | |||
| 495 | + | defer unix.Close(fd) | |||
| 496 | + | src := o.t.d.src | |||
| 497 | + | if o.server.protected() || o.t.state == targetDone || !src.alive() { | |||
| 498 | + | return | |||
| 499 | + | } | |||
| 500 | + | if slices.Contains(src.mimes, mime) { | |||
| 501 | + | src.send(mime, fd) | |||
| 502 | + | } | |||
| 503 | + | } | |||
| 504 | + | ||||
| 505 | + | func (*dndOffer) Destroy(*wayland.DataOffer) {} | |||
| 506 | + | ||||
| 507 | + | func (o *dndOffer) Finish(r *wayland.DataOffer) { | |||
| 508 | + | t := o.t | |||
| 509 | + | switch t.state { | |||
| 510 | + | case targetDone: | |||
| 511 | + | return | |||
| 512 | + | case targetActive: | |||
| 513 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish before the drop") | |||
| 514 | + | return | |||
| 515 | + | } | |||
| 516 | + | if t.action == 0 || t.action == uint32(wayland.DataDeviceManagerDndActionAsk) { | |||
| 517 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without a final action") | |||
| 518 | + | return | |||
| 519 | + | } | |||
| 520 | + | o.server.dndDropDone(t) | |||
| 521 | + | if src := t.d.src; src.alive() && src.res.Version() >= 3 { | |||
| 522 | + | src.res.SendDndFinished() | |||
| 523 | + | } | |||
| 524 | + | } | |||
| 525 | + | ||||
| 526 | + | func (o *dndOffer) SetActions(r *wayland.DataOffer, actions, preferred uint32) { | |||
| 527 | + | if actions&^dndAllActions != 0 { | |||
| 528 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidActionMask), "invalid action mask") | |||
| 529 | + | return | |||
| 530 | + | } | |||
| 531 | + | if preferred != 0 && (preferred&(preferred-1) != 0 || preferred&actions == 0) { | |||
| 532 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "invalid preferred action") | |||
| 533 | + | return | |||
| 534 | + | } | |||
| 535 | + | o.actions, o.preferred = actions, preferred | |||
| 536 | + | o.update() | |||
| 537 | + | } | |||
A internal/adapters/wayland/dnd_test.go +805 -0
| @@ -0,0 +1,805 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 10 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |
| 11 | + | "github.com/bnema/neferwl/internal/adapters/sessionsecurity" | |
| 12 | + | "github.com/bnema/neferwl/internal/ports" | |
| 13 | + | "github.com/bnema/wlturbo" | |
| 14 | + | "golang.org/x/sys/unix" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | const ( | |
| 18 | + | actCopy = uint32(wayland.DataDeviceManagerDndActionCopy) | |
| 19 | + | actMove = uint32(wayland.DataDeviceManagerDndActionMove) | |
| 20 | + | actAsk = uint32(wayland.DataDeviceManagerDndActionAsk) | |
| 21 | + | ) | |
| 22 | + | ||
| 23 | + | // dndEvent is one data device, offer or source event a test client saw. | |
| 24 | + | type dndEvent struct { | |
| 25 | + | name string | |
| 26 | + | mime string | |
| 27 | + | x, y float64 | |
| 28 | + | n uint32 | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | type dndDeviceEvents struct { | |
| 32 | + | wlturbo.BaseProxy | |
| 33 | + | c *wlturbo.Display | |
| 34 | + | log chan dndEvent | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | func (p *dndDeviceEvents) Dispatch(e *wlturbo.Event) { | |
| 38 | + | switch e.Opcode { | |
| 39 | + | case uint16(wayland.DataDeviceEventDataOffer): | |
| 40 | + | o := &dndOfferEvents{log: p.log} | |
| 41 | + | o.SetID(e.Uint32()) | |
| 42 | + | registerWireProxy(p.c, o) | |
| 43 | + | case uint16(wayland.DataDeviceEventEnter): | |
| 44 | + | serial, _ := e.Uint32(), e.Uint32() | |
| 45 | + | x, y := e.Fixed().Float64(), e.Fixed().Float64() | |
| 46 | + | p.log <- dndEvent{name: "enter", x: x, y: y, n: e.Uint32() + serial*0} | |
| 47 | + | case uint16(wayland.DataDeviceEventLeave): | |
| 48 | + | p.log <- dndEvent{name: "leave"} | |
| 49 | + | case uint16(wayland.DataDeviceEventMotion): | |
| 50 | + | _ = e.Uint32() | |
| 51 | + | p.log <- dndEvent{name: "motion", x: e.Fixed().Float64(), y: e.Fixed().Float64()} | |
| 52 | + | case uint16(wayland.DataDeviceEventDrop): | |
| 53 | + | p.log <- dndEvent{name: "drop"} | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | type dndOfferEvents struct { | |
| 58 | + | wlturbo.BaseProxy | |
| 59 | + | log chan dndEvent | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | func (p *dndOfferEvents) Dispatch(e *wlturbo.Event) { | |
| 63 | + | switch e.Opcode { | |
| 64 | + | case uint16(wayland.DataOfferEventOffer): | |
| 65 | + | p.log <- dndEvent{name: "offer", mime: e.String()} | |
| 66 | + | case uint16(wayland.DataOfferEventSourceActions): | |
| 67 | + | p.log <- dndEvent{name: "source_actions", n: e.Uint32()} | |
| 68 | + | case uint16(wayland.DataOfferEventAction): | |
| 69 | + | p.log <- dndEvent{name: "offer_action", n: e.Uint32()} | |
| 70 | + | } | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | type dndSourceEvents struct { | |
| 74 | + | wlturbo.BaseProxy | |
| 75 | + | log chan dndEvent | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func (p *dndSourceEvents) Dispatch(e *wlturbo.Event) { | |
| 79 | + | switch e.Opcode { | |
| 80 | + | case uint16(wayland.DataSourceEventTarget): | |
| 81 | + | p.log <- dndEvent{name: "target", mime: e.String()} | |
| 82 | + | case uint16(wayland.DataSourceEventSend): | |
| 83 | + | mime := e.String() | |
| 84 | + | fd := int(e.Fd()) | |
| 85 | + | _, _ = unix.Write(fd, []byte("dnd data")) | |
| 86 | + | _ = unix.Close(fd) | |
| 87 | + | p.log <- dndEvent{name: "send", mime: mime} | |
| 88 | + | case uint16(wayland.DataSourceEventCancelled): | |
| 89 | + | p.log <- dndEvent{name: "cancelled"} | |
| 90 | + | case uint16(wayland.DataSourceEventDndDropPerformed): | |
| 91 | + | p.log <- dndEvent{name: "drop_performed"} | |
| 92 | + | case uint16(wayland.DataSourceEventDndFinished): | |
| 93 | + | p.log <- dndEvent{name: "finished"} | |
| 94 | + | case uint16(wayland.DataSourceEventAction): | |
| 95 | + | p.log <- dndEvent{name: "action", n: e.Uint32()} | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // dndClient is a client with a toplevel, a pointer and a data device. | |
| 100 | + | type dndClient struct { | |
| 101 | + | c *wlturbo.Display | |
| 102 | + | manager uint32 | |
| 103 | + | device *dndDeviceEvents | |
| 104 | + | serials chan uint32 | |
| 105 | + | comp uint32 | |
| 106 | + | shm uint32 | |
| 107 | + | surf uint32 | |
| 108 | + | window ports.WindowID | |
| 109 | + | log chan dndEvent | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | func newDndClient(t *testing.T, s *Server, dir string, events <-chan ports.ClientEvent) *dndClient { | |
| 113 | + | t.Helper() | |
| 114 | + | c := protocolClient(t, s, dir) | |
| 115 | + | d := &dndClient{c: c, log: make(chan dndEvent, 64), serials: make(chan uint32, 8)} | |
| 116 | + | seat := bindVersion(t, c, "wl_seat", 8) | |
| 117 | + | registerProtocol(t, c, seat) | |
| 118 | + | ptr := &pressSerials{serials: d.serials} | |
| 119 | + | ptr.SetID(c.AllocateID()) | |
| 120 | + | registerWireProxy(c, ptr) | |
| 121 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, ptr.ID()) | |
| 122 | + | d.manager = bindVersion(t, c, "wl_data_device_manager", 3) | |
| 123 | + | d.device = &dndDeviceEvents{c: c, log: d.log} | |
| 124 | + | d.device.SetID(c.AllocateID()) | |
| 125 | + | registerWireProxy(c, d.device) | |
| 126 | + | requestProtocol(t, c, d.manager, wayland.DataDeviceManagerRequestGetDataDevice, d.device.ID(), seat) | |
| 127 | + | d.comp = bindVersion(t, c, "wl_compositor", 5) | |
| 128 | + | d.shm = bindProtocol(t, c, "wl_shm") | |
| 129 | + | registerProtocol(t, c, d.shm) | |
| 130 | + | w, surf, _ := surfaceMapperAt(t, c, events, 5)() | |
| 131 | + | d.window, d.surf = w.ID, surf | |
| 132 | + | return d | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | // next returns the next event the client sees. | |
| 136 | + | func (d *dndClient) next(t *testing.T) dndEvent { | |
| 137 | + | t.Helper() | |
| 138 | + | deadline := time.After(2 * time.Second) | |
| 139 | + | for { | |
| 140 | + | if err := d.c.Roundtrip(); err != nil { | |
| 141 | + | t.Fatal(err) | |
| 142 | + | } | |
| 143 | + | select { | |
| 144 | + | case ev := <-d.log: | |
| 145 | + | return ev | |
| 146 | + | case <-deadline: | |
| 147 | + | t.Fatal("no event") | |
| 148 | + | case <-time.After(5 * time.Millisecond): | |
| 149 | + | } | |
| 150 | + | } | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | // expect waits for the next event and checks its name. | |
| 154 | + | func (d *dndClient) expect(t *testing.T, name string) dndEvent { | |
| 155 | + | t.Helper() | |
| 156 | + | ev := d.next(t) | |
| 157 | + | if ev.name != name { | |
| 158 | + | t.Fatalf("event %+v, want %s", ev, name) | |
| 159 | + | } | |
| 160 | + | return ev | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | // none checks that no event arrives. | |
| 164 | + | func (d *dndClient) none(t *testing.T) { | |
| 165 | + | t.Helper() | |
| 166 | + | for range 2 { | |
| 167 | + | if err := d.c.Roundtrip(); err != nil { | |
| 168 | + | t.Fatal(err) | |
| 169 | + | } | |
| 170 | + | time.Sleep(30 * time.Millisecond) | |
| 171 | + | } | |
| 172 | + | select { | |
| 173 | + | case ev := <-d.log: | |
| 174 | + | t.Fatalf("unexpected event %+v", ev) | |
| 175 | + | default: | |
| 176 | + | } | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | func (d *dndClient) source(t *testing.T, actions uint32, mimes ...string) uint32 { | |
| 180 | + | t.Helper() | |
| 181 | + | src := &dndSourceEvents{log: d.log} | |
| 182 | + | src.SetID(d.c.AllocateID()) | |
| 183 | + | registerWireProxy(d.c, src) | |
| 184 | + | requestProtocol(t, d.c, d.manager, wayland.DataDeviceManagerRequestCreateDataSource, src.ID()) | |
| 185 | + | for _, m := range mimes { | |
| 186 | + | requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestOffer, m) | |
| 187 | + | } | |
| 188 | + | if actions != 0 { | |
| 189 | + | requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestSetActions, actions) | |
| 190 | + | } | |
| 191 | + | return src.ID() | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | // press makes the client's window press a button and returns the serial. | |
| 195 | + | func (d *dndClient) press(t *testing.T, commands chan<- ports.ClientCommand) uint32 { | |
| 196 | + | t.Helper() | |
| 197 | + | commands <- ports.PointerFocus{ID: d.window, X: 1, Y: 1} | |
| 198 | + | commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Pressed: true, Time: time.Millisecond} | |
| 199 | + | deadline := time.After(2 * time.Second) | |
| 200 | + | for { | |
| 201 | + | if err := d.c.Roundtrip(); err != nil { | |
| 202 | + | t.Fatal(err) | |
| 203 | + | } | |
| 204 | + | select { | |
| 205 | + | case serial := <-d.serials: | |
| 206 | + | return serial | |
| 207 | + | case <-deadline: | |
| 208 | + | t.Fatal("no button") | |
| 209 | + | case <-time.After(5 * time.Millisecond): | |
| 210 | + | } | |
| 211 | + | } | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | func (d *dndClient) startDrag(t *testing.T, src, icon, serial uint32) { | |
| 215 | + | t.Helper() | |
| 216 | + | requestProtocol(t, d.c, d.device.ID(), wayland.DataDeviceRequestStartDrag, src, d.surf, icon, serial) | |
| 217 | + | if err := d.c.Roundtrip(); err != nil { | |
| 218 | + | t.Fatal(err) | |
| 219 | + | } | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | // buffer makes a w×h ARGB shm buffer. | |
| 223 | + | func (d *dndClient) buffer(t *testing.T, w, h int) uint32 { | |
| 224 | + | t.Helper() | |
| 225 | + | fd, err := unix.MemfdCreate("dnd-icon", 0) | |
| 226 | + | if err != nil { | |
| 227 | + | t.Fatal(err) | |
| 228 | + | } | |
| 229 | + | defer unix.Close(fd) | |
| 230 | + | if err := unix.Ftruncate(fd, int64(w*h*4)); err != nil { | |
| 231 | + | t.Fatal(err) | |
| 232 | + | } | |
| 233 | + | pool, buf := d.c.AllocateID(), d.c.AllocateID() | |
| 234 | + | registerProtocol(t, d.c, pool) | |
| 235 | + | registerProtocol(t, d.c, buf) | |
| 236 | + | if err := wireRequest(d.c, d.shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(w*h*4)); err != nil { | |
| 237 | + | t.Fatal(err) | |
| 238 | + | } | |
| 239 | + | requestProtocol(t, d.c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(w), int32(h), int32(w*4), uint32(0)) | |
| 240 | + | return buf | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | func (d *dndClient) surfaceNew(t *testing.T) uint32 { | |
| 244 | + | t.Helper() | |
| 245 | + | id := d.c.AllocateID() | |
| 246 | + | requestProtocol(t, d.c, d.comp, wayland.CompositorRequestCreateSurface, id) | |
| 247 | + | registerProtocol(t, d.c, id) | |
| 248 | + | return id | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | // dndNextEvent returns the next client event of type T, skipping others. | |
| 252 | + | func dndNextEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T { | |
| 253 | + | t.Helper() | |
| 254 | + | deadline := time.After(2 * time.Second) | |
| 255 | + | for { | |
| 256 | + | select { | |
| 257 | + | case ev := <-events: | |
| 258 | + | if v, ok := ev.(T); ok { | |
| 259 | + | return v | |
| 260 | + | } | |
| 261 | + | case <-deadline: | |
| 262 | + | t.Fatalf("no %T event", *new(T)) | |
| 263 | + | } | |
| 264 | + | } | |
| 265 | + | } | |
| 266 | + | ||
| 267 | + | // dndNoEvent checks that no client event of type T is pending. | |
| 268 | + | func dndNoEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) { | |
| 269 | + | t.Helper() | |
| 270 | + | time.Sleep(50 * time.Millisecond) | |
| 271 | + | for { | |
| 272 | + | select { | |
| 273 | + | case ev := <-events: | |
| 274 | + | if _, ok := ev.(T); ok { | |
| 275 | + | t.Fatalf("unexpected %T: %+v", ev, ev) | |
| 276 | + | } | |
| 277 | + | default: | |
| 278 | + | return | |
| 279 | + | } | |
| 280 | + | } | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | // dndPair is two clients with a drag started from a on a source. | |
| 284 | + | type dndPair struct { | |
| 285 | + | s *Server | |
| 286 | + | events chan ports.ClientEvent | |
| 287 | + | commands chan ports.ClientCommand | |
| 288 | + | a, b *dndClient | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | func newDndPair(t *testing.T) *dndPair { | |
| 292 | + | t.Helper() | |
| 293 | + | s, events, commands, dir := keyboardServer(t) | |
| 294 | + | p := &dndPair{s: s, events: events, commands: commands} | |
| 295 | + | p.a = newDndClient(t, s, dir, events) | |
| 296 | + | p.b = newDndClient(t, s, dir, events) | |
| 297 | + | return p | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | // start begins a drag of a source offering text/plain, copy and move. | |
| 301 | + | func (p *dndPair) start(t *testing.T, icon uint32) uint32 { | |
| 302 | + | t.Helper() | |
| 303 | + | src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list") | |
| 304 | + | p.a.startDrag(t, src, icon, p.a.press(t, p.commands)) | |
| 305 | + | return src | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | // enter makes b the drag target and returns the offer id. | |
| 309 | + | func (p *dndPair) enter(t *testing.T) uint32 { | |
| 310 | + | t.Helper() | |
| 311 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6, Time: 10 * time.Millisecond} | |
| 312 | + | for _, want := range []dndEvent{{name: "offer", mime: "text/plain"}, {name: "offer", mime: "text/uri-list"}, {name: "source_actions", n: actCopy | actMove}} { | |
| 313 | + | if ev := p.b.next(t); ev != want { | |
| 314 | + | t.Fatalf("event %+v, want %+v", ev, want) | |
| 315 | + | } | |
| 316 | + | } | |
| 317 | + | ev := p.b.expect(t, "enter") | |
| 318 | + | if ev.x != 5 || ev.y != 6 || ev.n == 0 { | |
| 319 | + | t.Fatalf("enter %+v", ev) | |
| 320 | + | } | |
| 321 | + | return ev.n | |
| 322 | + | } | |
| 323 | + | ||
| 324 | + | // A1: a drag from a to b delivers enter, motion and drop with the offer; | |
| 325 | + | // the action is negotiated; the data reaches b; a is told every step. | |
| 326 | + | func TestDataDragBetweenClients(t *testing.T) { | |
| 327 | + | p := newDndPair(t) | |
| 328 | + | p.start(t, 0) | |
| 329 | + | if v := dndNextEvent[ports.DataDragStarted](t, p.events); v.Icon != 0 { | |
| 330 | + | t.Fatalf("started %+v", v) | |
| 331 | + | } | |
| 332 | + | offer := p.enter(t) | |
| 333 | + | ||
| 334 | + | p.commands <- ports.DataDragMotion{ID: p.b.window, X: 7, Y: 8, Time: 20 * time.Millisecond} | |
| 335 | + | if ev := p.b.expect(t, "motion"); ev.x != 7 || ev.y != 8 { | |
| 336 | + | t.Fatalf("motion %+v", ev) | |
| 337 | + | } | |
| 338 | + | ||
| 339 | + | // b accepts and prefers move: both sides learn the action. | |
| 340 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |
| 341 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove) | |
| 342 | + | if ev := p.a.expect(t, "target"); ev.mime != "text/plain" { | |
| 343 | + | t.Fatalf("target %+v", ev) | |
| 344 | + | } | |
| 345 | + | if ev := p.a.expect(t, "action"); ev.n != actMove { | |
| 346 | + | t.Fatalf("action %+v", ev) | |
| 347 | + | } | |
| 348 | + | if ev := p.b.expect(t, "offer_action"); ev.n != actMove { | |
| 349 | + | t.Fatalf("offer action %+v", ev) | |
| 350 | + | } | |
| 351 | + | ||
| 352 | + | // Finishing before the drop is a protocol error, so only after it. | |
| 353 | + | p.commands <- ports.DataDragDrop{} | |
| 354 | + | p.b.expect(t, "drop") | |
| 355 | + | p.a.expect(t, "drop_performed") | |
| 356 | + | ||
| 357 | + | var fds [2]int | |
| 358 | + | if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil { | |
| 359 | + | t.Fatal(err) | |
| 360 | + | } | |
| 361 | + | defer unix.Close(fds[0]) | |
| 362 | + | if err := wireRequest(p.b.c, offer, uint16(wayland.DataOfferRequestReceive), []int{fds[1]}, "text/plain"); err != nil { | |
| 363 | + | t.Fatal(err) | |
| 364 | + | } | |
| 365 | + | _ = unix.Close(fds[1]) | |
| 366 | + | if err := p.b.c.Roundtrip(); err != nil { | |
| 367 | + | t.Fatal(err) | |
| 368 | + | } | |
| 369 | + | if ev := p.a.expect(t, "send"); ev.mime != "text/plain" { | |
| 370 | + | t.Fatalf("send %+v", ev) | |
| 371 | + | } | |
| 372 | + | buf := make([]byte, 16) | |
| 373 | + | n, err := unix.Read(fds[0], buf) | |
| 374 | + | for errors.Is(err, unix.EINTR) { | |
| 375 | + | n, err = unix.Read(fds[0], buf) | |
| 376 | + | } | |
| 377 | + | if err != nil || string(buf[:n]) != "dnd data" { | |
| 378 | + | t.Fatalf("received %q, %v", buf[:max(n, 0)], err) | |
| 379 | + | } | |
| 380 | + | ||
| 381 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish) | |
| 382 | + | p.a.expect(t, "finished") | |
| 383 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy) | |
| 384 | + | p.a.none(t) | |
| 385 | + | p.b.none(t) | |
| 386 | + | // core ended the drag: wayland does not echo it. | |
| 387 | + | dndNoEvent[ports.DataDragEnded](t, p.events) | |
| 388 | + | } | |
| 389 | + | ||
| 390 | + | // The action follows wlroots: the preferred one if both allow it, else the | |
| 391 | + | // first of copy, move, ask they share, else none. | |
| 392 | + | func TestChooseAction(t *testing.T) { | |
| 393 | + | for _, tc := range []struct{ avail, preferred, want uint32 }{ | |
| 394 | + | {actCopy | actMove, actMove, actMove}, | |
| 395 | + | {actCopy | actMove, actAsk, actCopy}, | |
| 396 | + | {actMove | actAsk, 0, actMove}, | |
| 397 | + | {actAsk, 0, actAsk}, | |
| 398 | + | {0, actCopy, 0}, | |
| 399 | + | } { | |
| 400 | + | if got := chooseAction(tc.avail, tc.preferred); got != tc.want { | |
| 401 | + | t.Errorf("chooseAction(%d, %d) = %d, want %d", tc.avail, tc.preferred, got, tc.want) | |
| 402 | + | } | |
| 403 | + | } | |
| 404 | + | } | |
| 405 | + | ||
| 406 | + | // A target and a source sharing no action leave the drop refused. | |
| 407 | + | func TestDataDragNoCommonAction(t *testing.T) { | |
| 408 | + | p := newDndPair(t) | |
| 409 | + | src := p.a.source(t, actCopy, "text/plain") | |
| 410 | + | p.a.startDrag(t, src, 0, p.a.press(t, p.commands)) | |
| 411 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |
| 412 | + | p.b.expect(t, "offer") | |
| 413 | + | p.b.expect(t, "source_actions") | |
| 414 | + | offer := p.b.expect(t, "enter").n | |
| 415 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |
| 416 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actMove, actMove) | |
| 417 | + | p.a.expect(t, "target") | |
| 418 | + | p.commands <- ports.DataDragDrop{} | |
| 419 | + | p.b.expect(t, "leave") | |
| 420 | + | // The refused target no longer accepts. | |
| 421 | + | if ev := p.a.expect(t, "target"); ev.mime != "" { | |
| 422 | + | t.Fatalf("target %+v", ev) | |
| 423 | + | } | |
| 424 | + | p.a.expect(t, "cancelled") | |
| 425 | + | } | |
| 426 | + | ||
| 427 | + | // A2: a release over nothing, or over a target that accepted nothing, | |
| 428 | + | // cancels the source; the target gets leave and no drop. | |
| 429 | + | func TestDataDragRefusedDrop(t *testing.T) { | |
| 430 | + | t.Run("nothing", func(t *testing.T) { | |
| 431 | + | p := newDndPair(t) | |
| 432 | + | p.start(t, 0) | |
| 433 | + | p.commands <- ports.DataDragDrop{} | |
| 434 | + | p.a.expect(t, "cancelled") | |
| 435 | + | p.b.none(t) | |
| 436 | + | }) | |
| 437 | + | t.Run("not accepted", func(t *testing.T) { | |
| 438 | + | p := newDndPair(t) | |
| 439 | + | p.start(t, 0) | |
| 440 | + | p.enter(t) | |
| 441 | + | p.commands <- ports.DataDragDrop{} | |
| 442 | + | p.b.expect(t, "leave") | |
| 443 | + | p.a.expect(t, "cancelled") | |
| 444 | + | p.b.none(t) | |
| 445 | + | }) | |
| 446 | + | t.Run("escape", func(t *testing.T) { | |
| 447 | + | p := newDndPair(t) | |
| 448 | + | p.start(t, 0) | |
| 449 | + | p.enter(t) | |
| 450 | + | p.commands <- ports.DataDragCancel{} | |
| 451 | + | p.b.expect(t, "leave") | |
| 452 | + | p.a.expect(t, "cancelled") | |
| 453 | + | dndNoEvent[ports.DataDragEnded](t, p.events) | |
| 454 | + | }) | |
| 455 | + | } | |
| 456 | + | ||
| 457 | + | // Moving to another surface leaves the first and enters the second; a | |
| 458 | + | // surface of nobody (ID 0) leaves. | |
| 459 | + | func TestDataDragLeavesOnFocusChange(t *testing.T) { | |
| 460 | + | p := newDndPair(t) | |
| 461 | + | p.start(t, 0) | |
| 462 | + | p.enter(t) | |
| 463 | + | p.commands <- ports.DataDragFocus{} | |
| 464 | + | p.b.expect(t, "leave") | |
| 465 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 2, Y: 3} | |
| 466 | + | for range 3 { | |
| 467 | + | p.b.next(t) | |
| 468 | + | } | |
| 469 | + | if ev := p.b.expect(t, "enter"); ev.x != 2 || ev.y != 3 { | |
| 470 | + | t.Fatalf("enter %+v", ev) | |
| 471 | + | } | |
| 472 | + | } | |
| 473 | + | ||
| 474 | + | // A4: the source dying mid-drag ends it: the target gets leave and core a | |
| 475 | + | // DataDragEnded; later commands change nothing. | |
| 476 | + | func TestDataDragSourceDestroyed(t *testing.T) { | |
| 477 | + | p := newDndPair(t) | |
| 478 | + | src := p.start(t, 0) | |
| 479 | + | p.enter(t) | |
| 480 | + | requestProtocol(t, p.a.c, src, wayland.DataSourceRequestDestroy) | |
| 481 | + | if err := p.a.c.Roundtrip(); err != nil { | |
| 482 | + | t.Fatal(err) | |
| 483 | + | } | |
| 484 | + | p.b.expect(t, "leave") | |
| 485 | + | dndNextEvent[ports.DataDragEnded](t, p.events) | |
| 486 | + | p.commands <- ports.DataDragMotion{ID: p.b.window, X: 9, Y: 9} | |
| 487 | + | p.commands <- ports.DataDragDrop{} | |
| 488 | + | p.b.none(t) | |
| 489 | + | } | |
| 490 | + | ||
| 491 | + | // A drag whose origin client dies ends the same way. | |
| 492 | + | func TestDataDragOriginDies(t *testing.T) { | |
| 493 | + | p := newDndPair(t) | |
| 494 | + | p.start(t, 0) | |
| 495 | + | p.enter(t) | |
| 496 | + | _ = p.a.c.Close() | |
| 497 | + | p.b.expect(t, "leave") | |
| 498 | + | dndNextEvent[ports.DataDragEnded](t, p.events) | |
| 499 | + | } | |
| 500 | + | ||
| 501 | + | // A6: a start_drag without the current press of the origin client is | |
| 502 | + | // refused: the source is cancelled and core hears nothing. | |
| 503 | + | func TestDataDragInvalidStart(t *testing.T) { | |
| 504 | + | t.Run("stale serial", func(t *testing.T) { | |
| 505 | + | p := newDndPair(t) | |
| 506 | + | serial := p.a.press(t, p.commands) | |
| 507 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial-1) | |
| 508 | + | p.a.expect(t, "cancelled") | |
| 509 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 510 | + | }) | |
| 511 | + | t.Run("other client's press", func(t *testing.T) { | |
| 512 | + | p := newDndPair(t) | |
| 513 | + | p.a.press(t, p.commands) | |
| 514 | + | serial := p.b.press(t, p.commands) | |
| 515 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |
| 516 | + | p.a.expect(t, "cancelled") | |
| 517 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 518 | + | }) | |
| 519 | + | t.Run("used source", func(t *testing.T) { | |
| 520 | + | p := newDndPair(t) | |
| 521 | + | src := p.start(t, 0) | |
| 522 | + | p.commands <- ports.DataDragCancel{} | |
| 523 | + | p.a.expect(t, "cancelled") | |
| 524 | + | p.a.startDrag(t, src, 0, p.a.press(t, p.commands)) | |
| 525 | + | p.a.expect(t, "cancelled") | |
| 526 | + | }) | |
| 527 | + | t.Run("drag in progress", func(t *testing.T) { | |
| 528 | + | p := newDndPair(t) | |
| 529 | + | p.start(t, 0) | |
| 530 | + | dndNextEvent[ports.DataDragStarted](t, p.events) | |
| 531 | + | other := p.a.source(t, actCopy, "text/plain") | |
| 532 | + | p.a.startDrag(t, other, 0, p.a.press(t, p.commands)) | |
| 533 | + | p.a.expect(t, "cancelled") | |
| 534 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 535 | + | }) | |
| 536 | + | t.Run("icon with another role", func(t *testing.T) { | |
| 537 | + | p := newDndPair(t) | |
| 538 | + | serial := p.a.press(t, p.commands) | |
| 539 | + | src := p.a.source(t, actCopy, "text/plain") | |
| 540 | + | // The origin toplevel is a role already. | |
| 541 | + | requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestStartDrag, src, p.a.surf, p.a.surf, serial) | |
| 542 | + | expectProtocolError(t, p.a.c, p.a.device.ID(), uint32(wayland.DataDeviceErrorRole)) | |
| 543 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 544 | + | }) | |
| 545 | + | } | |
| 546 | + | ||
| 547 | + | // A nil source is a drag internal to the origin client: only its own | |
| 548 | + | // surfaces see it, without offer. | |
| 549 | + | func TestDataDragWithoutSource(t *testing.T) { | |
| 550 | + | p := newDndPair(t) | |
| 551 | + | p.a.startDrag(t, 0, 0, p.a.press(t, p.commands)) | |
| 552 | + | dndNextEvent[ports.DataDragStarted](t, p.events) | |
| 553 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |
| 554 | + | p.b.none(t) | |
| 555 | + | p.commands <- ports.DataDragFocus{ID: p.a.window, X: 4, Y: 5} | |
| 556 | + | if ev := p.a.expect(t, "enter"); ev.n != 0 || ev.x != 4 || ev.y != 5 { | |
| 557 | + | t.Fatalf("enter %+v", ev) | |
| 558 | + | } | |
| 559 | + | p.commands <- ports.DataDragDrop{} | |
| 560 | + | p.a.expect(t, "drop") | |
| 561 | + | } | |
| 562 | + | ||
| 563 | + | // Offer and source requests are validated. | |
| 564 | + | func TestDataDragProtocolErrors(t *testing.T) { | |
| 565 | + | t.Run("finish before drop", func(t *testing.T) { | |
| 566 | + | p := newDndPair(t) | |
| 567 | + | p.start(t, 0) | |
| 568 | + | offer := p.enter(t) | |
| 569 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish) | |
| 570 | + | expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidFinish)) | |
| 571 | + | }) | |
| 572 | + | t.Run("offer action mask", func(t *testing.T) { | |
| 573 | + | p := newDndPair(t) | |
| 574 | + | p.start(t, 0) | |
| 575 | + | offer := p.enter(t) | |
| 576 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, uint32(8), uint32(0)) | |
| 577 | + | expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidActionMask)) | |
| 578 | + | }) | |
| 579 | + | t.Run("offer preferred action", func(t *testing.T) { | |
| 580 | + | p := newDndPair(t) | |
| 581 | + | p.start(t, 0) | |
| 582 | + | offer := p.enter(t) | |
| 583 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actMove) | |
| 584 | + | expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidAction)) | |
| 585 | + | }) | |
| 586 | + | t.Run("source action mask", func(t *testing.T) { | |
| 587 | + | p := newDndPair(t) | |
| 588 | + | src := p.a.source(t, 0, "text/plain") | |
| 589 | + | requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, uint32(16)) | |
| 590 | + | expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidActionMask)) | |
| 591 | + | }) | |
| 592 | + | t.Run("selection with actions", func(t *testing.T) { | |
| 593 | + | p := newDndPair(t) | |
| 594 | + | src := p.a.source(t, actCopy, "text/plain") | |
| 595 | + | requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestSetSelection, src, uint32(0)) | |
| 596 | + | expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource)) | |
| 597 | + | }) | |
| 598 | + | t.Run("actions after the drag started", func(t *testing.T) { | |
| 599 | + | p := newDndPair(t) | |
| 600 | + | src := p.start(t, 0) | |
| 601 | + | requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, actCopy) | |
| 602 | + | expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource)) | |
| 603 | + | }) | |
| 604 | + | } | |
| 605 | + | ||
| 606 | + | // A destroyed offer before finish cancels the source. | |
| 607 | + | func TestDataDragOfferDestroyedBeforeFinish(t *testing.T) { | |
| 608 | + | p := newDndPair(t) | |
| 609 | + | p.start(t, 0) | |
| 610 | + | offer := p.enter(t) | |
| 611 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |
| 612 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy) | |
| 613 | + | p.a.expect(t, "target") | |
| 614 | + | p.a.expect(t, "action") | |
| 615 | + | p.commands <- ports.DataDragDrop{} | |
| 616 | + | p.b.expect(t, "offer_action") | |
| 617 | + | p.b.expect(t, "drop") | |
| 618 | + | p.a.expect(t, "drop_performed") | |
| 619 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy) | |
| 620 | + | p.a.expect(t, "cancelled") | |
| 621 | + | } | |
| 622 | + | ||
| 623 | + | // iconContent reports whether the server still holds content for id. | |
| 624 | + | func iconContent(t *testing.T, s *Server, id ports.WindowID) bool { | |
| 625 | + | t.Helper() | |
| 626 | + | var ok bool | |
| 627 | + | if !s.display.Do(func() { _, ok = s.latest[id] }) { | |
| 628 | + | t.Fatal("display stopped") | |
| 629 | + | } | |
| 630 | + | return ok | |
| 631 | + | } | |
| 632 | + | ||
| 633 | + | // The icon is a surface with its own window ID: its size and the | |
| 634 | + | // offsets of its commits reach core, its content is published, and both go | |
| 635 | + | // when the drag ends. | |
| 636 | + | func TestDataDragIcon(t *testing.T) { | |
| 637 | + | p := newDndPair(t) | |
| 638 | + | icon := p.a.surfaceNew(t) | |
| 639 | + | buf := p.a.buffer(t, 8, 6) | |
| 640 | + | // An offset on an ordinary surface is not used. | |
| 641 | + | requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestOffset, int32(5), int32(5)) | |
| 642 | + | requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestCommit) | |
| 643 | + | ||
| 644 | + | src := p.a.source(t, actCopy, "text/plain") | |
| 645 | + | p.a.startDrag(t, src, icon, p.a.press(t, p.commands)) | |
| 646 | + | started := dndNextEvent[ports.DataDragStarted](t, p.events) | |
| 647 | + | if started.Icon == 0 { | |
| 648 | + | t.Fatal("no icon ID") | |
| 649 | + | } | |
| 650 | + | id := started.Icon | |
| 651 | + | if iconContent(t, p.s, id) { | |
| 652 | + | t.Fatal("content before the first commit") | |
| 653 | + | } | |
| 654 | + | ||
| 655 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 656 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-3), int32(-4)) | |
| 657 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |
| 658 | + | if err := p.a.c.Roundtrip(); err != nil { | |
| 659 | + | t.Fatal(err) | |
| 660 | + | } | |
| 661 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -3, Y: -4, W: 8, H: 6}) { | |
| 662 | + | t.Fatalf("icon %+v", got) | |
| 663 | + | } | |
| 664 | + | if !iconContent(t, p.s, id) { | |
| 665 | + | t.Fatal("no icon content") | |
| 666 | + | } | |
| 667 | + | ||
| 668 | + | // Offsets add up from commit to commit. | |
| 669 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(2)) | |
| 670 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |
| 671 | + | if err := p.a.c.Roundtrip(); err != nil { | |
| 672 | + | t.Fatal(err) | |
| 673 | + | } | |
| 674 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -4, Y: -2, W: 8, H: 6}) { | |
| 675 | + | t.Fatalf("icon %+v", got) | |
| 676 | + | } | |
| 677 | + | // The icon never takes input. | |
| 678 | + | dndNoEvent[ports.InputRegionChanged](t, p.events) | |
| 679 | + | ||
| 680 | + | p.commands <- ports.DataDragCancel{} | |
| 681 | + | p.a.expect(t, "cancelled") | |
| 682 | + | if iconContent(t, p.s, id) { | |
| 683 | + | t.Fatal("icon content kept after the drag") | |
| 684 | + | } | |
| 685 | + | } | |
| 686 | + | ||
| 687 | + | // An icon committed before start_drag shows at once. | |
| 688 | + | func TestDataDragIconCommittedEarly(t *testing.T) { | |
| 689 | + | p := newDndPair(t) | |
| 690 | + | icon := p.a.surfaceNew(t) | |
| 691 | + | buf := p.a.buffer(t, 4, 4) | |
| 692 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 693 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |
| 694 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands)) | |
| 695 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |
| 696 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, W: 4, H: 4}) { | |
| 697 | + | t.Fatalf("icon %+v", got) | |
| 698 | + | } | |
| 699 | + | if !iconContent(t, p.s, id) { | |
| 700 | + | t.Fatal("no icon content") | |
| 701 | + | } | |
| 702 | + | } | |
| 703 | + | ||
| 704 | + | // Before wl_surface v5 the icon offset rides on attach and adds up. | |
| 705 | + | func TestDataDragIconAttachOffset(t *testing.T) { | |
| 706 | + | p := newDndPair(t) | |
| 707 | + | comp4 := bindVersion(t, p.a.c, "wl_compositor", 4) | |
| 708 | + | icon := p.a.c.AllocateID() | |
| 709 | + | requestProtocol(t, p.a.c, comp4, wayland.CompositorRequestCreateSurface, icon) | |
| 710 | + | registerProtocol(t, p.a.c, icon) | |
| 711 | + | buf := p.a.buffer(t, 4, 4) | |
| 712 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands)) | |
| 713 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |
| 714 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(-2), int32(-1)) | |
| 715 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |
| 716 | + | if err := p.a.c.Roundtrip(); err != nil { | |
| 717 | + | t.Fatal(err) | |
| 718 | + | } | |
| 719 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -2, Y: -1, W: 4, H: 4}) { | |
| 720 | + | t.Fatalf("icon %+v", got) | |
| 721 | + | } | |
| 722 | + | } | |
| 723 | + | ||
| 724 | + | // Destroying the icon mid-drag hides it; the drag goes on. | |
| 725 | + | func TestDataDragIconDestroyed(t *testing.T) { | |
| 726 | + | p := newDndPair(t) | |
| 727 | + | icon := p.a.surfaceNew(t) | |
| 728 | + | buf := p.a.buffer(t, 8, 6) | |
| 729 | + | p.a.startDrag(t, p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list"), icon, p.a.press(t, p.commands)) | |
| 730 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |
| 731 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 732 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |
| 733 | + | if err := p.a.c.Roundtrip(); err != nil { | |
| 734 | + | t.Fatal(err) | |
| 735 | + | } | |
| 736 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got.W != 8 { | |
| 737 | + | t.Fatalf("icon %+v", got) | |
| 738 | + | } | |
| 739 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestDestroy) | |
| 740 | + | if err := p.a.c.Roundtrip(); err != nil { | |
| 741 | + | t.Fatal(err) | |
| 742 | + | } | |
| 743 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id}) { | |
| 744 | + | t.Fatalf("icon %+v", got) | |
| 745 | + | } | |
| 746 | + | if iconContent(t, p.s, id) { | |
| 747 | + | t.Fatal("icon content kept") | |
| 748 | + | } | |
| 749 | + | p.enter(t) | |
| 750 | + | } | |
| 751 | + | ||
| 752 | + | // A7: the session lock cancels a drag at once: target leave, source | |
| 753 | + | // cancelled, icon gone. | |
| 754 | + | func TestDataDragLockCancels(t *testing.T) { | |
| 755 | + | gate := &sessionsecurity.Gate{} | |
| 756 | + | events := make(chan ports.ClientEvent, 128) | |
| 757 | + | commands := make(chan ports.ClientCommand, 32) | |
| 758 | + | dir := t.TempDir() | |
| 759 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, Security: gate, Keymap: keymapText(t)}, Channels{Events: events, Commands: commands, SecurityChanges: make(chan ports.SecurityState, 16), SecurityEvents: make(chan ports.SecurityBackendEvent, 16)}, logging.For(context.Background(), "wayland")) | |
| 760 | + | if err != nil { | |
| 761 | + | t.Fatal(err) | |
| 762 | + | } | |
| 763 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 764 | + | done := make(chan error, 1) | |
| 765 | + | go func() { done <- s.Run(ctx) }() | |
| 766 | + | t.Cleanup(func() { | |
| 767 | + | cancel() | |
| 768 | + | select { | |
| 769 | + | case err := <-done: | |
| 770 | + | if err != nil { | |
| 771 | + | t.Error(err) | |
| 772 | + | } | |
| 773 | + | case <-time.After(3 * time.Second): | |
| 774 | + | t.Error("server did not stop") | |
| 775 | + | } | |
| 776 | + | }) | |
| 777 | + | p := &dndPair{s: s, events: events, commands: commands} | |
| 778 | + | p.a = newDndClient(t, s, dir, events) | |
| 779 | + | p.b = newDndClient(t, s, dir, events) | |
| 780 | + | icon := p.a.surfaceNew(t) | |
| 781 | + | buf := p.a.buffer(t, 8, 6) | |
| 782 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 783 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |
| 784 | + | src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list") | |
| 785 | + | p.a.startDrag(t, src, icon, p.a.press(t, p.commands)) | |
| 786 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |
| 787 | + | p.enter(t) | |
| 788 | + | ||
| 789 | + | lc := protocolClient(t, s, dir) | |
| 790 | + | if _, err := bindLockManager(t, lc).Lock(); err != nil { | |
| 791 | + | t.Fatal(err) | |
| 792 | + | } | |
| 793 | + | if err := lc.Roundtrip(); err != nil { | |
| 794 | + | t.Fatal(err) | |
| 795 | + | } | |
| 796 | + | p.b.expect(t, "leave") | |
| 797 | + | p.a.expect(t, "cancelled") | |
| 798 | + | if iconContent(t, s, id) { | |
| 799 | + | t.Fatal("icon content kept") | |
| 800 | + | } | |
| 801 | + | // A new drag is refused while protected. | |
| 802 | + | src2 := p.a.source(t, actCopy, "text/plain") | |
| 803 | + | p.a.startDrag(t, src2, 0, uint32(1)) | |
| 804 | + | p.a.expect(t, "cancelled") | |
| 805 | + | } | |
| @@ -0,0 +1,805 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 10 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |||
| 11 | + | "github.com/bnema/neferwl/internal/adapters/sessionsecurity" | |||
| 12 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 13 | + | "github.com/bnema/wlturbo" | |||
| 14 | + | "golang.org/x/sys/unix" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | const ( | |||
| 18 | + | actCopy = uint32(wayland.DataDeviceManagerDndActionCopy) | |||
| 19 | + | actMove = uint32(wayland.DataDeviceManagerDndActionMove) | |||
| 20 | + | actAsk = uint32(wayland.DataDeviceManagerDndActionAsk) | |||
| 21 | + | ) | |||
| 22 | + | ||||
| 23 | + | // dndEvent is one data device, offer or source event a test client saw. | |||
| 24 | + | type dndEvent struct { | |||
| 25 | + | name string | |||
| 26 | + | mime string | |||
| 27 | + | x, y float64 | |||
| 28 | + | n uint32 | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | type dndDeviceEvents struct { | |||
| 32 | + | wlturbo.BaseProxy | |||
| 33 | + | c *wlturbo.Display | |||
| 34 | + | log chan dndEvent | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | func (p *dndDeviceEvents) Dispatch(e *wlturbo.Event) { | |||
| 38 | + | switch e.Opcode { | |||
| 39 | + | case uint16(wayland.DataDeviceEventDataOffer): | |||
| 40 | + | o := &dndOfferEvents{log: p.log} | |||
| 41 | + | o.SetID(e.Uint32()) | |||
| 42 | + | registerWireProxy(p.c, o) | |||
| 43 | + | case uint16(wayland.DataDeviceEventEnter): | |||
| 44 | + | serial, _ := e.Uint32(), e.Uint32() | |||
| 45 | + | x, y := e.Fixed().Float64(), e.Fixed().Float64() | |||
| 46 | + | p.log <- dndEvent{name: "enter", x: x, y: y, n: e.Uint32() + serial*0} | |||
| 47 | + | case uint16(wayland.DataDeviceEventLeave): | |||
| 48 | + | p.log <- dndEvent{name: "leave"} | |||
| 49 | + | case uint16(wayland.DataDeviceEventMotion): | |||
| 50 | + | _ = e.Uint32() | |||
| 51 | + | p.log <- dndEvent{name: "motion", x: e.Fixed().Float64(), y: e.Fixed().Float64()} | |||
| 52 | + | case uint16(wayland.DataDeviceEventDrop): | |||
| 53 | + | p.log <- dndEvent{name: "drop"} | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | type dndOfferEvents struct { | |||
| 58 | + | wlturbo.BaseProxy | |||
| 59 | + | log chan dndEvent | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | func (p *dndOfferEvents) Dispatch(e *wlturbo.Event) { | |||
| 63 | + | switch e.Opcode { | |||
| 64 | + | case uint16(wayland.DataOfferEventOffer): | |||
| 65 | + | p.log <- dndEvent{name: "offer", mime: e.String()} | |||
| 66 | + | case uint16(wayland.DataOfferEventSourceActions): | |||
| 67 | + | p.log <- dndEvent{name: "source_actions", n: e.Uint32()} | |||
| 68 | + | case uint16(wayland.DataOfferEventAction): | |||
| 69 | + | p.log <- dndEvent{name: "offer_action", n: e.Uint32()} | |||
| 70 | + | } | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | type dndSourceEvents struct { | |||
| 74 | + | wlturbo.BaseProxy | |||
| 75 | + | log chan dndEvent | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func (p *dndSourceEvents) Dispatch(e *wlturbo.Event) { | |||
| 79 | + | switch e.Opcode { | |||
| 80 | + | case uint16(wayland.DataSourceEventTarget): | |||
| 81 | + | p.log <- dndEvent{name: "target", mime: e.String()} | |||
| 82 | + | case uint16(wayland.DataSourceEventSend): | |||
| 83 | + | mime := e.String() | |||
| 84 | + | fd := int(e.Fd()) | |||
| 85 | + | _, _ = unix.Write(fd, []byte("dnd data")) | |||
| 86 | + | _ = unix.Close(fd) | |||
| 87 | + | p.log <- dndEvent{name: "send", mime: mime} | |||
| 88 | + | case uint16(wayland.DataSourceEventCancelled): | |||
| 89 | + | p.log <- dndEvent{name: "cancelled"} | |||
| 90 | + | case uint16(wayland.DataSourceEventDndDropPerformed): | |||
| 91 | + | p.log <- dndEvent{name: "drop_performed"} | |||
| 92 | + | case uint16(wayland.DataSourceEventDndFinished): | |||
| 93 | + | p.log <- dndEvent{name: "finished"} | |||
| 94 | + | case uint16(wayland.DataSourceEventAction): | |||
| 95 | + | p.log <- dndEvent{name: "action", n: e.Uint32()} | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // dndClient is a client with a toplevel, a pointer and a data device. | |||
| 100 | + | type dndClient struct { | |||
| 101 | + | c *wlturbo.Display | |||
| 102 | + | manager uint32 | |||
| 103 | + | device *dndDeviceEvents | |||
| 104 | + | serials chan uint32 | |||
| 105 | + | comp uint32 | |||
| 106 | + | shm uint32 | |||
| 107 | + | surf uint32 | |||
| 108 | + | window ports.WindowID | |||
| 109 | + | log chan dndEvent | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | func newDndClient(t *testing.T, s *Server, dir string, events <-chan ports.ClientEvent) *dndClient { | |||
| 113 | + | t.Helper() | |||
| 114 | + | c := protocolClient(t, s, dir) | |||
| 115 | + | d := &dndClient{c: c, log: make(chan dndEvent, 64), serials: make(chan uint32, 8)} | |||
| 116 | + | seat := bindVersion(t, c, "wl_seat", 8) | |||
| 117 | + | registerProtocol(t, c, seat) | |||
| 118 | + | ptr := &pressSerials{serials: d.serials} | |||
| 119 | + | ptr.SetID(c.AllocateID()) | |||
| 120 | + | registerWireProxy(c, ptr) | |||
| 121 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, ptr.ID()) | |||
| 122 | + | d.manager = bindVersion(t, c, "wl_data_device_manager", 3) | |||
| 123 | + | d.device = &dndDeviceEvents{c: c, log: d.log} | |||
| 124 | + | d.device.SetID(c.AllocateID()) | |||
| 125 | + | registerWireProxy(c, d.device) | |||
| 126 | + | requestProtocol(t, c, d.manager, wayland.DataDeviceManagerRequestGetDataDevice, d.device.ID(), seat) | |||
| 127 | + | d.comp = bindVersion(t, c, "wl_compositor", 5) | |||
| 128 | + | d.shm = bindProtocol(t, c, "wl_shm") | |||
| 129 | + | registerProtocol(t, c, d.shm) | |||
| 130 | + | w, surf, _ := surfaceMapperAt(t, c, events, 5)() | |||
| 131 | + | d.window, d.surf = w.ID, surf | |||
| 132 | + | return d | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | // next returns the next event the client sees. | |||
| 136 | + | func (d *dndClient) next(t *testing.T) dndEvent { | |||
| 137 | + | t.Helper() | |||
| 138 | + | deadline := time.After(2 * time.Second) | |||
| 139 | + | for { | |||
| 140 | + | if err := d.c.Roundtrip(); err != nil { | |||
| 141 | + | t.Fatal(err) | |||
| 142 | + | } | |||
| 143 | + | select { | |||
| 144 | + | case ev := <-d.log: | |||
| 145 | + | return ev | |||
| 146 | + | case <-deadline: | |||
| 147 | + | t.Fatal("no event") | |||
| 148 | + | case <-time.After(5 * time.Millisecond): | |||
| 149 | + | } | |||
| 150 | + | } | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | // expect waits for the next event and checks its name. | |||
| 154 | + | func (d *dndClient) expect(t *testing.T, name string) dndEvent { | |||
| 155 | + | t.Helper() | |||
| 156 | + | ev := d.next(t) | |||
| 157 | + | if ev.name != name { | |||
| 158 | + | t.Fatalf("event %+v, want %s", ev, name) | |||
| 159 | + | } | |||
| 160 | + | return ev | |||
| 161 | + | } | |||
| 162 | + | ||||
| 163 | + | // none checks that no event arrives. | |||
| 164 | + | func (d *dndClient) none(t *testing.T) { | |||
| 165 | + | t.Helper() | |||
| 166 | + | for range 2 { | |||
| 167 | + | if err := d.c.Roundtrip(); err != nil { | |||
| 168 | + | t.Fatal(err) | |||
| 169 | + | } | |||
| 170 | + | time.Sleep(30 * time.Millisecond) | |||
| 171 | + | } | |||
| 172 | + | select { | |||
| 173 | + | case ev := <-d.log: | |||
| 174 | + | t.Fatalf("unexpected event %+v", ev) | |||
| 175 | + | default: | |||
| 176 | + | } | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | func (d *dndClient) source(t *testing.T, actions uint32, mimes ...string) uint32 { | |||
| 180 | + | t.Helper() | |||
| 181 | + | src := &dndSourceEvents{log: d.log} | |||
| 182 | + | src.SetID(d.c.AllocateID()) | |||
| 183 | + | registerWireProxy(d.c, src) | |||
| 184 | + | requestProtocol(t, d.c, d.manager, wayland.DataDeviceManagerRequestCreateDataSource, src.ID()) | |||
| 185 | + | for _, m := range mimes { | |||
| 186 | + | requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestOffer, m) | |||
| 187 | + | } | |||
| 188 | + | if actions != 0 { | |||
| 189 | + | requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestSetActions, actions) | |||
| 190 | + | } | |||
| 191 | + | return src.ID() | |||
| 192 | + | } | |||
| 193 | + | ||||
| 194 | + | // press makes the client's window press a button and returns the serial. | |||
| 195 | + | func (d *dndClient) press(t *testing.T, commands chan<- ports.ClientCommand) uint32 { | |||
| 196 | + | t.Helper() | |||
| 197 | + | commands <- ports.PointerFocus{ID: d.window, X: 1, Y: 1} | |||
| 198 | + | commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Pressed: true, Time: time.Millisecond} | |||
| 199 | + | deadline := time.After(2 * time.Second) | |||
| 200 | + | for { | |||
| 201 | + | if err := d.c.Roundtrip(); err != nil { | |||
| 202 | + | t.Fatal(err) | |||
| 203 | + | } | |||
| 204 | + | select { | |||
| 205 | + | case serial := <-d.serials: | |||
| 206 | + | return serial | |||
| 207 | + | case <-deadline: | |||
| 208 | + | t.Fatal("no button") | |||
| 209 | + | case <-time.After(5 * time.Millisecond): | |||
| 210 | + | } | |||
| 211 | + | } | |||
| 212 | + | } | |||
| 213 | + | ||||
| 214 | + | func (d *dndClient) startDrag(t *testing.T, src, icon, serial uint32) { | |||
| 215 | + | t.Helper() | |||
| 216 | + | requestProtocol(t, d.c, d.device.ID(), wayland.DataDeviceRequestStartDrag, src, d.surf, icon, serial) | |||
| 217 | + | if err := d.c.Roundtrip(); err != nil { | |||
| 218 | + | t.Fatal(err) | |||
| 219 | + | } | |||
| 220 | + | } | |||
| 221 | + | ||||
| 222 | + | // buffer makes a w×h ARGB shm buffer. | |||
| 223 | + | func (d *dndClient) buffer(t *testing.T, w, h int) uint32 { | |||
| 224 | + | t.Helper() | |||
| 225 | + | fd, err := unix.MemfdCreate("dnd-icon", 0) | |||
| 226 | + | if err != nil { | |||
| 227 | + | t.Fatal(err) | |||
| 228 | + | } | |||
| 229 | + | defer unix.Close(fd) | |||
| 230 | + | if err := unix.Ftruncate(fd, int64(w*h*4)); err != nil { | |||
| 231 | + | t.Fatal(err) | |||
| 232 | + | } | |||
| 233 | + | pool, buf := d.c.AllocateID(), d.c.AllocateID() | |||
| 234 | + | registerProtocol(t, d.c, pool) | |||
| 235 | + | registerProtocol(t, d.c, buf) | |||
| 236 | + | if err := wireRequest(d.c, d.shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(w*h*4)); err != nil { | |||
| 237 | + | t.Fatal(err) | |||
| 238 | + | } | |||
| 239 | + | requestProtocol(t, d.c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(w), int32(h), int32(w*4), uint32(0)) | |||
| 240 | + | return buf | |||
| 241 | + | } | |||
| 242 | + | ||||
| 243 | + | func (d *dndClient) surfaceNew(t *testing.T) uint32 { | |||
| 244 | + | t.Helper() | |||
| 245 | + | id := d.c.AllocateID() | |||
| 246 | + | requestProtocol(t, d.c, d.comp, wayland.CompositorRequestCreateSurface, id) | |||
| 247 | + | registerProtocol(t, d.c, id) | |||
| 248 | + | return id | |||
| 249 | + | } | |||
| 250 | + | ||||
| 251 | + | // dndNextEvent returns the next client event of type T, skipping others. | |||
| 252 | + | func dndNextEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T { | |||
| 253 | + | t.Helper() | |||
| 254 | + | deadline := time.After(2 * time.Second) | |||
| 255 | + | for { | |||
| 256 | + | select { | |||
| 257 | + | case ev := <-events: | |||
| 258 | + | if v, ok := ev.(T); ok { | |||
| 259 | + | return v | |||
| 260 | + | } | |||
| 261 | + | case <-deadline: | |||
| 262 | + | t.Fatalf("no %T event", *new(T)) | |||
| 263 | + | } | |||
| 264 | + | } | |||
| 265 | + | } | |||
| 266 | + | ||||
| 267 | + | // dndNoEvent checks that no client event of type T is pending. | |||
| 268 | + | func dndNoEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) { | |||
| 269 | + | t.Helper() | |||
| 270 | + | time.Sleep(50 * time.Millisecond) | |||
| 271 | + | for { | |||
| 272 | + | select { | |||
| 273 | + | case ev := <-events: | |||
| 274 | + | if _, ok := ev.(T); ok { | |||
| 275 | + | t.Fatalf("unexpected %T: %+v", ev, ev) | |||
| 276 | + | } | |||
| 277 | + | default: | |||
| 278 | + | return | |||
| 279 | + | } | |||
| 280 | + | } | |||
| 281 | + | } | |||
| 282 | + | ||||
| 283 | + | // dndPair is two clients with a drag started from a on a source. | |||
| 284 | + | type dndPair struct { | |||
| 285 | + | s *Server | |||
| 286 | + | events chan ports.ClientEvent | |||
| 287 | + | commands chan ports.ClientCommand | |||
| 288 | + | a, b *dndClient | |||
| 289 | + | } | |||
| 290 | + | ||||
| 291 | + | func newDndPair(t *testing.T) *dndPair { | |||
| 292 | + | t.Helper() | |||
| 293 | + | s, events, commands, dir := keyboardServer(t) | |||
| 294 | + | p := &dndPair{s: s, events: events, commands: commands} | |||
| 295 | + | p.a = newDndClient(t, s, dir, events) | |||
| 296 | + | p.b = newDndClient(t, s, dir, events) | |||
| 297 | + | return p | |||
| 298 | + | } | |||
| 299 | + | ||||
| 300 | + | // start begins a drag of a source offering text/plain, copy and move. | |||
| 301 | + | func (p *dndPair) start(t *testing.T, icon uint32) uint32 { | |||
| 302 | + | t.Helper() | |||
| 303 | + | src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list") | |||
| 304 | + | p.a.startDrag(t, src, icon, p.a.press(t, p.commands)) | |||
| 305 | + | return src | |||
| 306 | + | } | |||
| 307 | + | ||||
| 308 | + | // enter makes b the drag target and returns the offer id. | |||
| 309 | + | func (p *dndPair) enter(t *testing.T) uint32 { | |||
| 310 | + | t.Helper() | |||
| 311 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6, Time: 10 * time.Millisecond} | |||
| 312 | + | for _, want := range []dndEvent{{name: "offer", mime: "text/plain"}, {name: "offer", mime: "text/uri-list"}, {name: "source_actions", n: actCopy | actMove}} { | |||
| 313 | + | if ev := p.b.next(t); ev != want { | |||
| 314 | + | t.Fatalf("event %+v, want %+v", ev, want) | |||
| 315 | + | } | |||
| 316 | + | } | |||
| 317 | + | ev := p.b.expect(t, "enter") | |||
| 318 | + | if ev.x != 5 || ev.y != 6 || ev.n == 0 { | |||
| 319 | + | t.Fatalf("enter %+v", ev) | |||
| 320 | + | } | |||
| 321 | + | return ev.n | |||
| 322 | + | } | |||
| 323 | + | ||||
| 324 | + | // A1: a drag from a to b delivers enter, motion and drop with the offer; | |||
| 325 | + | // the action is negotiated; the data reaches b; a is told every step. | |||
| 326 | + | func TestDataDragBetweenClients(t *testing.T) { | |||
| 327 | + | p := newDndPair(t) | |||
| 328 | + | p.start(t, 0) | |||
| 329 | + | if v := dndNextEvent[ports.DataDragStarted](t, p.events); v.Icon != 0 { | |||
| 330 | + | t.Fatalf("started %+v", v) | |||
| 331 | + | } | |||
| 332 | + | offer := p.enter(t) | |||
| 333 | + | ||||
| 334 | + | p.commands <- ports.DataDragMotion{ID: p.b.window, X: 7, Y: 8, Time: 20 * time.Millisecond} | |||
| 335 | + | if ev := p.b.expect(t, "motion"); ev.x != 7 || ev.y != 8 { | |||
| 336 | + | t.Fatalf("motion %+v", ev) | |||
| 337 | + | } | |||
| 338 | + | ||||
| 339 | + | // b accepts and prefers move: both sides learn the action. | |||
| 340 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |||
| 341 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove) | |||
| 342 | + | if ev := p.a.expect(t, "target"); ev.mime != "text/plain" { | |||
| 343 | + | t.Fatalf("target %+v", ev) | |||
| 344 | + | } | |||
| 345 | + | if ev := p.a.expect(t, "action"); ev.n != actMove { | |||
| 346 | + | t.Fatalf("action %+v", ev) | |||
| 347 | + | } | |||
| 348 | + | if ev := p.b.expect(t, "offer_action"); ev.n != actMove { | |||
| 349 | + | t.Fatalf("offer action %+v", ev) | |||
| 350 | + | } | |||
| 351 | + | ||||
| 352 | + | // Finishing before the drop is a protocol error, so only after it. | |||
| 353 | + | p.commands <- ports.DataDragDrop{} | |||
| 354 | + | p.b.expect(t, "drop") | |||
| 355 | + | p.a.expect(t, "drop_performed") | |||
| 356 | + | ||||
| 357 | + | var fds [2]int | |||
| 358 | + | if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil { | |||
| 359 | + | t.Fatal(err) | |||
| 360 | + | } | |||
| 361 | + | defer unix.Close(fds[0]) | |||
| 362 | + | if err := wireRequest(p.b.c, offer, uint16(wayland.DataOfferRequestReceive), []int{fds[1]}, "text/plain"); err != nil { | |||
| 363 | + | t.Fatal(err) | |||
| 364 | + | } | |||
| 365 | + | _ = unix.Close(fds[1]) | |||
| 366 | + | if err := p.b.c.Roundtrip(); err != nil { | |||
| 367 | + | t.Fatal(err) | |||
| 368 | + | } | |||
| 369 | + | if ev := p.a.expect(t, "send"); ev.mime != "text/plain" { | |||
| 370 | + | t.Fatalf("send %+v", ev) | |||
| 371 | + | } | |||
| 372 | + | buf := make([]byte, 16) | |||
| 373 | + | n, err := unix.Read(fds[0], buf) | |||
| 374 | + | for errors.Is(err, unix.EINTR) { | |||
| 375 | + | n, err = unix.Read(fds[0], buf) | |||
| 376 | + | } | |||
| 377 | + | if err != nil || string(buf[:n]) != "dnd data" { | |||
| 378 | + | t.Fatalf("received %q, %v", buf[:max(n, 0)], err) | |||
| 379 | + | } | |||
| 380 | + | ||||
| 381 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish) | |||
| 382 | + | p.a.expect(t, "finished") | |||
| 383 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy) | |||
| 384 | + | p.a.none(t) | |||
| 385 | + | p.b.none(t) | |||
| 386 | + | // core ended the drag: wayland does not echo it. | |||
| 387 | + | dndNoEvent[ports.DataDragEnded](t, p.events) | |||
| 388 | + | } | |||
| 389 | + | ||||
| 390 | + | // The action follows wlroots: the preferred one if both allow it, else the | |||
| 391 | + | // first of copy, move, ask they share, else none. | |||
| 392 | + | func TestChooseAction(t *testing.T) { | |||
| 393 | + | for _, tc := range []struct{ avail, preferred, want uint32 }{ | |||
| 394 | + | {actCopy | actMove, actMove, actMove}, | |||
| 395 | + | {actCopy | actMove, actAsk, actCopy}, | |||
| 396 | + | {actMove | actAsk, 0, actMove}, | |||
| 397 | + | {actAsk, 0, actAsk}, | |||
| 398 | + | {0, actCopy, 0}, | |||
| 399 | + | } { | |||
| 400 | + | if got := chooseAction(tc.avail, tc.preferred); got != tc.want { | |||
| 401 | + | t.Errorf("chooseAction(%d, %d) = %d, want %d", tc.avail, tc.preferred, got, tc.want) | |||
| 402 | + | } | |||
| 403 | + | } | |||
| 404 | + | } | |||
| 405 | + | ||||
| 406 | + | // A target and a source sharing no action leave the drop refused. | |||
| 407 | + | func TestDataDragNoCommonAction(t *testing.T) { | |||
| 408 | + | p := newDndPair(t) | |||
| 409 | + | src := p.a.source(t, actCopy, "text/plain") | |||
| 410 | + | p.a.startDrag(t, src, 0, p.a.press(t, p.commands)) | |||
| 411 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |||
| 412 | + | p.b.expect(t, "offer") | |||
| 413 | + | p.b.expect(t, "source_actions") | |||
| 414 | + | offer := p.b.expect(t, "enter").n | |||
| 415 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |||
| 416 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actMove, actMove) | |||
| 417 | + | p.a.expect(t, "target") | |||
| 418 | + | p.commands <- ports.DataDragDrop{} | |||
| 419 | + | p.b.expect(t, "leave") | |||
| 420 | + | // The refused target no longer accepts. | |||
| 421 | + | if ev := p.a.expect(t, "target"); ev.mime != "" { | |||
| 422 | + | t.Fatalf("target %+v", ev) | |||
| 423 | + | } | |||
| 424 | + | p.a.expect(t, "cancelled") | |||
| 425 | + | } | |||
| 426 | + | ||||
| 427 | + | // A2: a release over nothing, or over a target that accepted nothing, | |||
| 428 | + | // cancels the source; the target gets leave and no drop. | |||
| 429 | + | func TestDataDragRefusedDrop(t *testing.T) { | |||
| 430 | + | t.Run("nothing", func(t *testing.T) { | |||
| 431 | + | p := newDndPair(t) | |||
| 432 | + | p.start(t, 0) | |||
| 433 | + | p.commands <- ports.DataDragDrop{} | |||
| 434 | + | p.a.expect(t, "cancelled") | |||
| 435 | + | p.b.none(t) | |||
| 436 | + | }) | |||
| 437 | + | t.Run("not accepted", func(t *testing.T) { | |||
| 438 | + | p := newDndPair(t) | |||
| 439 | + | p.start(t, 0) | |||
| 440 | + | p.enter(t) | |||
| 441 | + | p.commands <- ports.DataDragDrop{} | |||
| 442 | + | p.b.expect(t, "leave") | |||
| 443 | + | p.a.expect(t, "cancelled") | |||
| 444 | + | p.b.none(t) | |||
| 445 | + | }) | |||
| 446 | + | t.Run("escape", func(t *testing.T) { | |||
| 447 | + | p := newDndPair(t) | |||
| 448 | + | p.start(t, 0) | |||
| 449 | + | p.enter(t) | |||
| 450 | + | p.commands <- ports.DataDragCancel{} | |||
| 451 | + | p.b.expect(t, "leave") | |||
| 452 | + | p.a.expect(t, "cancelled") | |||
| 453 | + | dndNoEvent[ports.DataDragEnded](t, p.events) | |||
| 454 | + | }) | |||
| 455 | + | } | |||
| 456 | + | ||||
| 457 | + | // Moving to another surface leaves the first and enters the second; a | |||
| 458 | + | // surface of nobody (ID 0) leaves. | |||
| 459 | + | func TestDataDragLeavesOnFocusChange(t *testing.T) { | |||
| 460 | + | p := newDndPair(t) | |||
| 461 | + | p.start(t, 0) | |||
| 462 | + | p.enter(t) | |||
| 463 | + | p.commands <- ports.DataDragFocus{} | |||
| 464 | + | p.b.expect(t, "leave") | |||
| 465 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 2, Y: 3} | |||
| 466 | + | for range 3 { | |||
| 467 | + | p.b.next(t) | |||
| 468 | + | } | |||
| 469 | + | if ev := p.b.expect(t, "enter"); ev.x != 2 || ev.y != 3 { | |||
| 470 | + | t.Fatalf("enter %+v", ev) | |||
| 471 | + | } | |||
| 472 | + | } | |||
| 473 | + | ||||
| 474 | + | // A4: the source dying mid-drag ends it: the target gets leave and core a | |||
| 475 | + | // DataDragEnded; later commands change nothing. | |||
| 476 | + | func TestDataDragSourceDestroyed(t *testing.T) { | |||
| 477 | + | p := newDndPair(t) | |||
| 478 | + | src := p.start(t, 0) | |||
| 479 | + | p.enter(t) | |||
| 480 | + | requestProtocol(t, p.a.c, src, wayland.DataSourceRequestDestroy) | |||
| 481 | + | if err := p.a.c.Roundtrip(); err != nil { | |||
| 482 | + | t.Fatal(err) | |||
| 483 | + | } | |||
| 484 | + | p.b.expect(t, "leave") | |||
| 485 | + | dndNextEvent[ports.DataDragEnded](t, p.events) | |||
| 486 | + | p.commands <- ports.DataDragMotion{ID: p.b.window, X: 9, Y: 9} | |||
| 487 | + | p.commands <- ports.DataDragDrop{} | |||
| 488 | + | p.b.none(t) | |||
| 489 | + | } | |||
| 490 | + | ||||
| 491 | + | // A drag whose origin client dies ends the same way. | |||
| 492 | + | func TestDataDragOriginDies(t *testing.T) { | |||
| 493 | + | p := newDndPair(t) | |||
| 494 | + | p.start(t, 0) | |||
| 495 | + | p.enter(t) | |||
| 496 | + | _ = p.a.c.Close() | |||
| 497 | + | p.b.expect(t, "leave") | |||
| 498 | + | dndNextEvent[ports.DataDragEnded](t, p.events) | |||
| 499 | + | } | |||
| 500 | + | ||||
| 501 | + | // A6: a start_drag without the current press of the origin client is | |||
| 502 | + | // refused: the source is cancelled and core hears nothing. | |||
| 503 | + | func TestDataDragInvalidStart(t *testing.T) { | |||
| 504 | + | t.Run("stale serial", func(t *testing.T) { | |||
| 505 | + | p := newDndPair(t) | |||
| 506 | + | serial := p.a.press(t, p.commands) | |||
| 507 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial-1) | |||
| 508 | + | p.a.expect(t, "cancelled") | |||
| 509 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |||
| 510 | + | }) | |||
| 511 | + | t.Run("other client's press", func(t *testing.T) { | |||
| 512 | + | p := newDndPair(t) | |||
| 513 | + | p.a.press(t, p.commands) | |||
| 514 | + | serial := p.b.press(t, p.commands) | |||
| 515 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |||
| 516 | + | p.a.expect(t, "cancelled") | |||
| 517 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |||
| 518 | + | }) | |||
| 519 | + | t.Run("used source", func(t *testing.T) { | |||
| 520 | + | p := newDndPair(t) | |||
| 521 | + | src := p.start(t, 0) | |||
| 522 | + | p.commands <- ports.DataDragCancel{} | |||
| 523 | + | p.a.expect(t, "cancelled") | |||
| 524 | + | p.a.startDrag(t, src, 0, p.a.press(t, p.commands)) | |||
| 525 | + | p.a.expect(t, "cancelled") | |||
| 526 | + | }) | |||
| 527 | + | t.Run("drag in progress", func(t *testing.T) { | |||
| 528 | + | p := newDndPair(t) | |||
| 529 | + | p.start(t, 0) | |||
| 530 | + | dndNextEvent[ports.DataDragStarted](t, p.events) | |||
| 531 | + | other := p.a.source(t, actCopy, "text/plain") | |||
| 532 | + | p.a.startDrag(t, other, 0, p.a.press(t, p.commands)) | |||
| 533 | + | p.a.expect(t, "cancelled") | |||
| 534 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |||
| 535 | + | }) | |||
| 536 | + | t.Run("icon with another role", func(t *testing.T) { | |||
| 537 | + | p := newDndPair(t) | |||
| 538 | + | serial := p.a.press(t, p.commands) | |||
| 539 | + | src := p.a.source(t, actCopy, "text/plain") | |||
| 540 | + | // The origin toplevel is a role already. | |||
| 541 | + | requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestStartDrag, src, p.a.surf, p.a.surf, serial) | |||
| 542 | + | expectProtocolError(t, p.a.c, p.a.device.ID(), uint32(wayland.DataDeviceErrorRole)) | |||
| 543 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |||
| 544 | + | }) | |||
| 545 | + | } | |||
| 546 | + | ||||
| 547 | + | // A nil source is a drag internal to the origin client: only its own | |||
| 548 | + | // surfaces see it, without offer. | |||
| 549 | + | func TestDataDragWithoutSource(t *testing.T) { | |||
| 550 | + | p := newDndPair(t) | |||
| 551 | + | p.a.startDrag(t, 0, 0, p.a.press(t, p.commands)) | |||
| 552 | + | dndNextEvent[ports.DataDragStarted](t, p.events) | |||
| 553 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |||
| 554 | + | p.b.none(t) | |||
| 555 | + | p.commands <- ports.DataDragFocus{ID: p.a.window, X: 4, Y: 5} | |||
| 556 | + | if ev := p.a.expect(t, "enter"); ev.n != 0 || ev.x != 4 || ev.y != 5 { | |||
| 557 | + | t.Fatalf("enter %+v", ev) | |||
| 558 | + | } | |||
| 559 | + | p.commands <- ports.DataDragDrop{} | |||
| 560 | + | p.a.expect(t, "drop") | |||
| 561 | + | } | |||
| 562 | + | ||||
| 563 | + | // Offer and source requests are validated. | |||
| 564 | + | func TestDataDragProtocolErrors(t *testing.T) { | |||
| 565 | + | t.Run("finish before drop", func(t *testing.T) { | |||
| 566 | + | p := newDndPair(t) | |||
| 567 | + | p.start(t, 0) | |||
| 568 | + | offer := p.enter(t) | |||
| 569 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish) | |||
| 570 | + | expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidFinish)) | |||
| 571 | + | }) | |||
| 572 | + | t.Run("offer action mask", func(t *testing.T) { | |||
| 573 | + | p := newDndPair(t) | |||
| 574 | + | p.start(t, 0) | |||
| 575 | + | offer := p.enter(t) | |||
| 576 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, uint32(8), uint32(0)) | |||
| 577 | + | expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidActionMask)) | |||
| 578 | + | }) | |||
| 579 | + | t.Run("offer preferred action", func(t *testing.T) { | |||
| 580 | + | p := newDndPair(t) | |||
| 581 | + | p.start(t, 0) | |||
| 582 | + | offer := p.enter(t) | |||
| 583 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actMove) | |||
| 584 | + | expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidAction)) | |||
| 585 | + | }) | |||
| 586 | + | t.Run("source action mask", func(t *testing.T) { | |||
| 587 | + | p := newDndPair(t) | |||
| 588 | + | src := p.a.source(t, 0, "text/plain") | |||
| 589 | + | requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, uint32(16)) | |||
| 590 | + | expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidActionMask)) | |||
| 591 | + | }) | |||
| 592 | + | t.Run("selection with actions", func(t *testing.T) { | |||
| 593 | + | p := newDndPair(t) | |||
| 594 | + | src := p.a.source(t, actCopy, "text/plain") | |||
| 595 | + | requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestSetSelection, src, uint32(0)) | |||
| 596 | + | expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource)) | |||
| 597 | + | }) | |||
| 598 | + | t.Run("actions after the drag started", func(t *testing.T) { | |||
| 599 | + | p := newDndPair(t) | |||
| 600 | + | src := p.start(t, 0) | |||
| 601 | + | requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, actCopy) | |||
| 602 | + | expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource)) | |||
| 603 | + | }) | |||
| 604 | + | } | |||
| 605 | + | ||||
| 606 | + | // A destroyed offer before finish cancels the source. | |||
| 607 | + | func TestDataDragOfferDestroyedBeforeFinish(t *testing.T) { | |||
| 608 | + | p := newDndPair(t) | |||
| 609 | + | p.start(t, 0) | |||
| 610 | + | offer := p.enter(t) | |||
| 611 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |||
| 612 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy) | |||
| 613 | + | p.a.expect(t, "target") | |||
| 614 | + | p.a.expect(t, "action") | |||
| 615 | + | p.commands <- ports.DataDragDrop{} | |||
| 616 | + | p.b.expect(t, "offer_action") | |||
| 617 | + | p.b.expect(t, "drop") | |||
| 618 | + | p.a.expect(t, "drop_performed") | |||
| 619 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy) | |||
| 620 | + | p.a.expect(t, "cancelled") | |||
| 621 | + | } | |||
| 622 | + | ||||
| 623 | + | // iconContent reports whether the server still holds content for id. | |||
| 624 | + | func iconContent(t *testing.T, s *Server, id ports.WindowID) bool { | |||
| 625 | + | t.Helper() | |||
| 626 | + | var ok bool | |||
| 627 | + | if !s.display.Do(func() { _, ok = s.latest[id] }) { | |||
| 628 | + | t.Fatal("display stopped") | |||
| 629 | + | } | |||
| 630 | + | return ok | |||
| 631 | + | } | |||
| 632 | + | ||||
| 633 | + | // The icon is a surface with its own window ID: its size and the | |||
| 634 | + | // offsets of its commits reach core, its content is published, and both go | |||
| 635 | + | // when the drag ends. | |||
| 636 | + | func TestDataDragIcon(t *testing.T) { | |||
| 637 | + | p := newDndPair(t) | |||
| 638 | + | icon := p.a.surfaceNew(t) | |||
| 639 | + | buf := p.a.buffer(t, 8, 6) | |||
| 640 | + | // An offset on an ordinary surface is not used. | |||
| 641 | + | requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestOffset, int32(5), int32(5)) | |||
| 642 | + | requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestCommit) | |||
| 643 | + | ||||
| 644 | + | src := p.a.source(t, actCopy, "text/plain") | |||
| 645 | + | p.a.startDrag(t, src, icon, p.a.press(t, p.commands)) | |||
| 646 | + | started := dndNextEvent[ports.DataDragStarted](t, p.events) | |||
| 647 | + | if started.Icon == 0 { | |||
| 648 | + | t.Fatal("no icon ID") | |||
| 649 | + | } | |||
| 650 | + | id := started.Icon | |||
| 651 | + | if iconContent(t, p.s, id) { | |||
| 652 | + | t.Fatal("content before the first commit") | |||
| 653 | + | } | |||
| 654 | + | ||||
| 655 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 656 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-3), int32(-4)) | |||
| 657 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |||
| 658 | + | if err := p.a.c.Roundtrip(); err != nil { | |||
| 659 | + | t.Fatal(err) | |||
| 660 | + | } | |||
| 661 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -3, Y: -4, W: 8, H: 6}) { | |||
| 662 | + | t.Fatalf("icon %+v", got) | |||
| 663 | + | } | |||
| 664 | + | if !iconContent(t, p.s, id) { | |||
| 665 | + | t.Fatal("no icon content") | |||
| 666 | + | } | |||
| 667 | + | ||||
| 668 | + | // Offsets add up from commit to commit. | |||
| 669 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(2)) | |||
| 670 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |||
| 671 | + | if err := p.a.c.Roundtrip(); err != nil { | |||
| 672 | + | t.Fatal(err) | |||
| 673 | + | } | |||
| 674 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -4, Y: -2, W: 8, H: 6}) { | |||
| 675 | + | t.Fatalf("icon %+v", got) | |||
| 676 | + | } | |||
| 677 | + | // The icon never takes input. | |||
| 678 | + | dndNoEvent[ports.InputRegionChanged](t, p.events) | |||
| 679 | + | ||||
| 680 | + | p.commands <- ports.DataDragCancel{} | |||
| 681 | + | p.a.expect(t, "cancelled") | |||
| 682 | + | if iconContent(t, p.s, id) { | |||
| 683 | + | t.Fatal("icon content kept after the drag") | |||
| 684 | + | } | |||
| 685 | + | } | |||
| 686 | + | ||||
| 687 | + | // An icon committed before start_drag shows at once. | |||
| 688 | + | func TestDataDragIconCommittedEarly(t *testing.T) { | |||
| 689 | + | p := newDndPair(t) | |||
| 690 | + | icon := p.a.surfaceNew(t) | |||
| 691 | + | buf := p.a.buffer(t, 4, 4) | |||
| 692 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 693 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |||
| 694 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands)) | |||
| 695 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |||
| 696 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, W: 4, H: 4}) { | |||
| 697 | + | t.Fatalf("icon %+v", got) | |||
| 698 | + | } | |||
| 699 | + | if !iconContent(t, p.s, id) { | |||
| 700 | + | t.Fatal("no icon content") | |||
| 701 | + | } | |||
| 702 | + | } | |||
| 703 | + | ||||
| 704 | + | // Before wl_surface v5 the icon offset rides on attach and adds up. | |||
| 705 | + | func TestDataDragIconAttachOffset(t *testing.T) { | |||
| 706 | + | p := newDndPair(t) | |||
| 707 | + | comp4 := bindVersion(t, p.a.c, "wl_compositor", 4) | |||
| 708 | + | icon := p.a.c.AllocateID() | |||
| 709 | + | requestProtocol(t, p.a.c, comp4, wayland.CompositorRequestCreateSurface, icon) | |||
| 710 | + | registerProtocol(t, p.a.c, icon) | |||
| 711 | + | buf := p.a.buffer(t, 4, 4) | |||
| 712 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands)) | |||
| 713 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |||
| 714 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(-2), int32(-1)) | |||
| 715 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |||
| 716 | + | if err := p.a.c.Roundtrip(); err != nil { | |||
| 717 | + | t.Fatal(err) | |||
| 718 | + | } | |||
| 719 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -2, Y: -1, W: 4, H: 4}) { | |||
| 720 | + | t.Fatalf("icon %+v", got) | |||
| 721 | + | } | |||
| 722 | + | } | |||
| 723 | + | ||||
| 724 | + | // Destroying the icon mid-drag hides it; the drag goes on. | |||
| 725 | + | func TestDataDragIconDestroyed(t *testing.T) { | |||
| 726 | + | p := newDndPair(t) | |||
| 727 | + | icon := p.a.surfaceNew(t) | |||
| 728 | + | buf := p.a.buffer(t, 8, 6) | |||
| 729 | + | p.a.startDrag(t, p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list"), icon, p.a.press(t, p.commands)) | |||
| 730 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |||
| 731 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 732 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |||
| 733 | + | if err := p.a.c.Roundtrip(); err != nil { | |||
| 734 | + | t.Fatal(err) | |||
| 735 | + | } | |||
| 736 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got.W != 8 { | |||
| 737 | + | t.Fatalf("icon %+v", got) | |||
| 738 | + | } | |||
| 739 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestDestroy) | |||
| 740 | + | if err := p.a.c.Roundtrip(); err != nil { | |||
| 741 | + | t.Fatal(err) | |||
| 742 | + | } | |||
| 743 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id}) { | |||
| 744 | + | t.Fatalf("icon %+v", got) | |||
| 745 | + | } | |||
| 746 | + | if iconContent(t, p.s, id) { | |||
| 747 | + | t.Fatal("icon content kept") | |||
| 748 | + | } | |||
| 749 | + | p.enter(t) | |||
| 750 | + | } | |||
| 751 | + | ||||
| 752 | + | // A7: the session lock cancels a drag at once: target leave, source | |||
| 753 | + | // cancelled, icon gone. | |||
| 754 | + | func TestDataDragLockCancels(t *testing.T) { | |||
| 755 | + | gate := &sessionsecurity.Gate{} | |||
| 756 | + | events := make(chan ports.ClientEvent, 128) | |||
| 757 | + | commands := make(chan ports.ClientCommand, 32) | |||
| 758 | + | dir := t.TempDir() | |||
| 759 | + | s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, Security: gate, Keymap: keymapText(t)}, Channels{Events: events, Commands: commands, SecurityChanges: make(chan ports.SecurityState, 16), SecurityEvents: make(chan ports.SecurityBackendEvent, 16)}, logging.For(context.Background(), "wayland")) | |||
| 760 | + | if err != nil { | |||
| 761 | + | t.Fatal(err) | |||
| 762 | + | } | |||
| 763 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 764 | + | done := make(chan error, 1) | |||
| 765 | + | go func() { done <- s.Run(ctx) }() | |||
| 766 | + | t.Cleanup(func() { | |||
| 767 | + | cancel() | |||
| 768 | + | select { | |||
| 769 | + | case err := <-done: | |||
| 770 | + | if err != nil { | |||
| 771 | + | t.Error(err) | |||
| 772 | + | } | |||
| 773 | + | case <-time.After(3 * time.Second): | |||
| 774 | + | t.Error("server did not stop") | |||
| 775 | + | } | |||
| 776 | + | }) | |||
| 777 | + | p := &dndPair{s: s, events: events, commands: commands} | |||
| 778 | + | p.a = newDndClient(t, s, dir, events) | |||
| 779 | + | p.b = newDndClient(t, s, dir, events) | |||
| 780 | + | icon := p.a.surfaceNew(t) | |||
| 781 | + | buf := p.a.buffer(t, 8, 6) | |||
| 782 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 783 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |||
| 784 | + | src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list") | |||
| 785 | + | p.a.startDrag(t, src, icon, p.a.press(t, p.commands)) | |||
| 786 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |||
| 787 | + | p.enter(t) | |||
| 788 | + | ||||
| 789 | + | lc := protocolClient(t, s, dir) | |||
| 790 | + | if _, err := bindLockManager(t, lc).Lock(); err != nil { | |||
| 791 | + | t.Fatal(err) | |||
| 792 | + | } | |||
| 793 | + | if err := lc.Roundtrip(); err != nil { | |||
| 794 | + | t.Fatal(err) | |||
| 795 | + | } | |||
| 796 | + | p.b.expect(t, "leave") | |||
| 797 | + | p.a.expect(t, "cancelled") | |||
| 798 | + | if iconContent(t, s, id) { | |||
| 799 | + | t.Fatal("icon content kept") | |||
| 800 | + | } | |||
| 801 | + | // A new drag is refused while protected. | |||
| 802 | + | src2 := p.a.source(t, actCopy, "text/plain") | |||
| 803 | + | p.a.startDrag(t, src2, 0, uint32(1)) | |||
| 804 | + | p.a.expect(t, "cancelled") | |||
| 805 | + | } | |||
M internal/adapters/wayland/input_region.go +2 -1
| @@ -82,7 +82,8 @@ func (s *surface) resetInputEmission() { | |||
| 82 | 82 | func (s *surface) emitInput() { | |
| 83 | 83 | root := s.root() | |
| 84 | 84 | id := root.windowID() | |
| 85 | - | if id == 0 { | |
| 85 | + | if id == 0 || root.kind == roleDragIcon { | |
| 86 | + | // A drag icon takes no input. | |
| 86 | 87 | return | |
| 87 | 88 | } | |
| 88 | 89 | all, rects := root.effectiveInput() | |
| @@ -82,7 +82,8 @@ func (s *surface) resetInputEmission() { | |||||
| 82 | func (s *surface) emitInput() { | 82 | func (s *surface) emitInput() { | ||
| 83 | root := s.root() | 83 | root := s.root() | ||
| 84 | id := root.windowID() | 84 | id := root.windowID() | ||
| 85 | - | if id == 0 { | 85 | + | if id == 0 || root.kind == roleDragIcon { |
| 86 | + | // A drag icon takes no input. | |||
| 86 | return | 87 | return | ||
| 87 | } | 88 | } | ||
| 88 | all, rects := root.effectiveInput() | 89 | all, rects := root.effectiveInput() | ||
M internal/adapters/wayland/server.go +10 -2
| @@ -292,7 +292,12 @@ type Server struct { | |||
| 292 | 292 | cursorQueued bool | |
| 293 | 293 | cursorReady chan struct{} | |
| 294 | 294 | // Clipboard and primary selection (clipboard.go). | |
| 295 | - | selections [2]*clipSource | |
| 295 | + | selections [2]*clipSource | |
| 296 | + | // Drag and drop (dnd.go): the drag in progress, drops waiting for | |
| 297 | + | // their finish, and each client's data devices. | |
| 298 | + | dnd *dndState | |
| 299 | + | dndDrops []*dndTarget | |
| 300 | + | dataDevices map[server.Client][]*wayland.DataDevice | |
| 296 | 301 | clipDevices []*clipDevice | |
| 297 | 302 | dataSources, primarySources, controlSources map[*server.Resource]*clipSource | |
| 298 | 303 | // Pointer constraints (constraints.go). | |
| @@ -786,13 +791,16 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 786 | 791 | // Only input/focus commands require an owner envelope. Config relay | |
| 787 | 792 | // also produces SetKeymap and other epoch-independent commands. | |
| 788 | 793 | switch cmd.(type) { | |
| 789 | - | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey: | |
| 794 | + | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey, | |
| 795 | + | ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel: | |
| 790 | 796 | return | |
| 791 | 797 | } | |
| 792 | 798 | } | |
| 793 | 799 | switch c := cmd.(type) { | |
| 794 | 800 | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey: | |
| 795 | 801 | s.applyInput(c) | |
| 802 | + | case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel: | |
| 803 | + | s.applyDrag(c) | |
| 796 | 804 | case ports.ConfigurePopup: | |
| 797 | 805 | if w := s.windows[c.ID]; w != nil && w.popup != nil { | |
| 798 | 806 | w.popup.configure(c) | |
| @@ -292,7 +292,12 @@ type Server struct { | |||||
| 292 | cursorQueued bool | 292 | cursorQueued bool | ||
| 293 | cursorReady chan struct{} | 293 | cursorReady chan struct{} | ||
| 294 | // Clipboard and primary selection (clipboard.go). | 294 | // Clipboard and primary selection (clipboard.go). | ||
| 295 | - | selections [2]*clipSource | 295 | + | selections [2]*clipSource |
| 296 | + | // Drag and drop (dnd.go): the drag in progress, drops waiting for | |||
| 297 | + | // their finish, and each client's data devices. | |||
| 298 | + | dnd *dndState | |||
| 299 | + | dndDrops []*dndTarget | |||
| 300 | + | dataDevices map[server.Client][]*wayland.DataDevice | |||
| 296 | clipDevices []*clipDevice | 301 | clipDevices []*clipDevice | ||
| 297 | dataSources, primarySources, controlSources map[*server.Resource]*clipSource | 302 | dataSources, primarySources, controlSources map[*server.Resource]*clipSource | ||
| 298 | // Pointer constraints (constraints.go). | 303 | // Pointer constraints (constraints.go). | ||
| @@ -786,13 +791,16 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 786 | // Only input/focus commands require an owner envelope. Config relay | 791 | // Only input/focus commands require an owner envelope. Config relay | ||
| 787 | // also produces SetKeymap and other epoch-independent commands. | 792 | // also produces SetKeymap and other epoch-independent commands. | ||
| 788 | switch cmd.(type) { | 793 | switch cmd.(type) { | ||
| 789 | - | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey: | 794 | + | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey, |
| 795 | + | ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel: | |||
| 790 | return | 796 | return | ||
| 791 | } | 797 | } | ||
| 792 | } | 798 | } | ||
| 793 | switch c := cmd.(type) { | 799 | switch c := cmd.(type) { | ||
| 794 | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey: | 800 | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey: | ||
| 795 | s.applyInput(c) | 801 | s.applyInput(c) | ||
| 802 | + | case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel: | |||
| 803 | + | s.applyDrag(c) | |||
| 796 | case ports.ConfigurePopup: | 804 | case ports.ConfigurePopup: | ||
| 797 | if w := s.windows[c.ID]; w != nil && w.popup != nil { | 805 | if w := s.windows[c.ID]; w != nil && w.popup != nil { | ||
| 798 | w.popup.configure(c) | 806 | w.popup.configure(c) | ||
M internal/adapters/wayland/session_lock.go +1 -0
| @@ -138,6 +138,7 @@ func (l *sessionLock) orphan() { | |||
| 138 | 138 | } | |
| 139 | 139 | ||
| 140 | 140 | func (s *Server) resetProtectionInput() { | |
| 141 | + | s.dndCancelAll() | |
| 141 | 142 | s.changeFocus(0) | |
| 142 | 143 | s.changePointerFocus(0, 0, 0) | |
| 143 | 144 | s.deactivateText() | |
| @@ -138,6 +138,7 @@ func (l *sessionLock) orphan() { | |||||
| 138 | } | 138 | } | ||
| 139 | 139 | ||||
| 140 | func (s *Server) resetProtectionInput() { | 140 | func (s *Server) resetProtectionInput() { | ||
| 141 | + | s.dndCancelAll() | |||
| 141 | s.changeFocus(0) | 142 | s.changeFocus(0) | ||
| 142 | s.changePointerFocus(0, 0, 0) | 143 | s.changePointerFocus(0, 0, 0) | ||
| 143 | s.deactivateText() | 144 | s.deactivateText() | ||
M internal/adapters/wayland/surface.go +25 -3
| @@ -26,6 +26,8 @@ const ( | |||
| 26 | 26 | roleCursor | |
| 27 | 27 | roleInputPopup | |
| 28 | 28 | roleSessionLock | |
| 29 | + | // roleDragIcon is the icon of a data drag (dnd.go). | |
| 30 | + | roleDragIcon | |
| 29 | 31 | ) | |
| 30 | 32 | ||
| 31 | 33 | type surface struct { | |
| @@ -137,6 +139,9 @@ type pendingCommit struct { | |||
| 137 | 139 | // barrier and wait are fifo requests; at the commit timestamp. | |
| 138 | 140 | barrier, wait bool | |
| 139 | 141 | at time.Time | |
| 142 | + | // dx, dy accumulate the attach offsets and wl_surface.offset requests; | |
| 143 | + | // only a drag icon uses them. | |
| 144 | + | dx, dy int32 | |
| 140 | 145 | // damage is in surface (logical) pixels, bufDamage in buffer pixels. | |
| 141 | 146 | damage, bufDamage []ports.Rect | |
| 142 | 147 | // feedback are wp_presentation feedbacks for this commit. | |
| @@ -192,6 +197,9 @@ func (s *surface) windowID() ports.WindowID { | |||
| 192 | 197 | if s.lock != nil && s.lock.mapped { | |
| 193 | 198 | return s.lock.id | |
| 194 | 199 | } | |
| 200 | + | if s.kind == roleDragIcon && s.server.dnd != nil && s.server.dnd.icon == s { | |
| 201 | + | return s.server.dnd.iconID | |
| 202 | + | } | |
| 195 | 203 | return 0 | |
| 196 | 204 | } | |
| 197 | 205 | ||
| @@ -343,10 +351,17 @@ func (s *surface) Destroy(*wayland.Surface) { | |||
| 343 | 351 | if c := s.server.constraints[s]; c != nil { | |
| 344 | 352 | s.server.dropConstraint(c) | |
| 345 | 353 | } | |
| 354 | + | s.server.dndSurfaceGone(s) | |
| 346 | 355 | } | |
| 347 | - | func (s *surface) Attach(_ *wayland.Surface, b *wayland.Buffer, _, _ int32) { | |
| 356 | + | ||
| 357 | + | // Attach: before wl_surface v5 the offset rides on attach; only a drag | |
| 358 | + | // icon uses it. | |
| 359 | + | func (s *surface) Attach(r *wayland.Surface, b *wayland.Buffer, dx, dy int32) { | |
| 348 | 360 | s.next.buffer = b | |
| 349 | 361 | s.next.attached = true | |
| 362 | + | if r.Version() < 5 { | |
| 363 | + | s.next.dx, s.next.dy = s.next.dx+dx, s.next.dy+dy | |
| 364 | + | } | |
| 350 | 365 | } | |
| 351 | 366 | func (s *surface) Frame(r *wayland.Surface, id uint32) { | |
| 352 | 367 | cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{}) | |
| @@ -544,6 +559,9 @@ func (s *surface) applyCommit(u *update) { | |||
| 544 | 559 | } | |
| 545 | 560 | s.commitConstraint(u.cons, u.region, geometry) | |
| 546 | 561 | s.emitInput() | |
| 562 | + | if s.kind == roleDragIcon { | |
| 563 | + | s.server.dndIconCommit(s, u.dx, u.dy) | |
| 564 | + | } | |
| 547 | 565 | if s.xdg != nil && s.xdg.window != nil { | |
| 548 | 566 | s.xdg.window.afterCommit() | |
| 549 | 567 | } | |
| @@ -757,5 +775,9 @@ func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | |||
| 757 | 775 | } | |
| 758 | 776 | s.next.scale = int(v) | |
| 759 | 777 | } | |
| 760 | - | func (*surface) Offset(*wayland.Surface, int32, int32) {} | |
| 761 | - | func (*surface) GetRelease(*wayland.Surface, uint32) {} | |
| 778 | + | ||
| 779 | + | // Offset moves the next commit's surface (v5); only a drag icon uses it. | |
| 780 | + | func (s *surface) Offset(_ *wayland.Surface, dx, dy int32) { | |
| 781 | + | s.next.dx, s.next.dy = s.next.dx+dx, s.next.dy+dy | |
| 782 | + | } | |
| 783 | + | func (*surface) GetRelease(*wayland.Surface, uint32) {} | |
| @@ -26,6 +26,8 @@ const ( | |||||
| 26 | roleCursor | 26 | roleCursor | ||
| 27 | roleInputPopup | 27 | roleInputPopup | ||
| 28 | roleSessionLock | 28 | roleSessionLock | ||
| 29 | + | // roleDragIcon is the icon of a data drag (dnd.go). | |||
| 30 | + | roleDragIcon | |||
| 29 | ) | 31 | ) | ||
| 30 | 32 | ||||
| 31 | type surface struct { | 33 | type surface struct { | ||
| @@ -137,6 +139,9 @@ type pendingCommit struct { | |||||
| 137 | // barrier and wait are fifo requests; at the commit timestamp. | 139 | // barrier and wait are fifo requests; at the commit timestamp. | ||
| 138 | barrier, wait bool | 140 | barrier, wait bool | ||
| 139 | at time.Time | 141 | at time.Time | ||
| 142 | + | // dx, dy accumulate the attach offsets and wl_surface.offset requests; | |||
| 143 | + | // only a drag icon uses them. | |||
| 144 | + | dx, dy int32 | |||
| 140 | // damage is in surface (logical) pixels, bufDamage in buffer pixels. | 145 | // damage is in surface (logical) pixels, bufDamage in buffer pixels. | ||
| 141 | damage, bufDamage []ports.Rect | 146 | damage, bufDamage []ports.Rect | ||
| 142 | // feedback are wp_presentation feedbacks for this commit. | 147 | // feedback are wp_presentation feedbacks for this commit. | ||
| @@ -192,6 +197,9 @@ func (s *surface) windowID() ports.WindowID { | |||||
| 192 | if s.lock != nil && s.lock.mapped { | 197 | if s.lock != nil && s.lock.mapped { | ||
| 193 | return s.lock.id | 198 | return s.lock.id | ||
| 194 | } | 199 | } | ||
| 200 | + | if s.kind == roleDragIcon && s.server.dnd != nil && s.server.dnd.icon == s { | |||
| 201 | + | return s.server.dnd.iconID | |||
| 202 | + | } | |||
| 195 | return 0 | 203 | return 0 | ||
| 196 | } | 204 | } | ||
| 197 | 205 | ||||
| @@ -343,10 +351,17 @@ func (s *surface) Destroy(*wayland.Surface) { | |||||
| 343 | if c := s.server.constraints[s]; c != nil { | 351 | if c := s.server.constraints[s]; c != nil { | ||
| 344 | s.server.dropConstraint(c) | 352 | s.server.dropConstraint(c) | ||
| 345 | } | 353 | } | ||
| 354 | + | s.server.dndSurfaceGone(s) | |||
| 346 | } | 355 | } | ||
| 347 | - | func (s *surface) Attach(_ *wayland.Surface, b *wayland.Buffer, _, _ int32) { | 356 | + | |
| 357 | + | // Attach: before wl_surface v5 the offset rides on attach; only a drag | |||
| 358 | + | // icon uses it. | |||
| 359 | + | func (s *surface) Attach(r *wayland.Surface, b *wayland.Buffer, dx, dy int32) { | |||
| 348 | s.next.buffer = b | 360 | s.next.buffer = b | ||
| 349 | s.next.attached = true | 361 | s.next.attached = true | ||
| 362 | + | if r.Version() < 5 { | |||
| 363 | + | s.next.dx, s.next.dy = s.next.dx+dx, s.next.dy+dy | |||
| 364 | + | } | |||
| 350 | } | 365 | } | ||
| 351 | func (s *surface) Frame(r *wayland.Surface, id uint32) { | 366 | func (s *surface) Frame(r *wayland.Surface, id uint32) { | ||
| 352 | cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{}) | 367 | cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{}) | ||
| @@ -544,6 +559,9 @@ func (s *surface) applyCommit(u *update) { | |||||
| 544 | } | 559 | } | ||
| 545 | s.commitConstraint(u.cons, u.region, geometry) | 560 | s.commitConstraint(u.cons, u.region, geometry) | ||
| 546 | s.emitInput() | 561 | s.emitInput() | ||
| 562 | + | if s.kind == roleDragIcon { | |||
| 563 | + | s.server.dndIconCommit(s, u.dx, u.dy) | |||
| 564 | + | } | |||
| 547 | if s.xdg != nil && s.xdg.window != nil { | 565 | if s.xdg != nil && s.xdg.window != nil { | ||
| 548 | s.xdg.window.afterCommit() | 566 | s.xdg.window.afterCommit() | ||
| 549 | } | 567 | } | ||
| @@ -757,5 +775,9 @@ func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | |||||
| 757 | } | 775 | } | ||
| 758 | s.next.scale = int(v) | 776 | s.next.scale = int(v) | ||
| 759 | } | 777 | } | ||
| 760 | - | func (*surface) Offset(*wayland.Surface, int32, int32) {} | 778 | + | |
| 761 | - | func (*surface) GetRelease(*wayland.Surface, uint32) {} | 779 | + | // Offset moves the next commit's surface (v5); only a drag icon uses it. |
| 780 | + | func (s *surface) Offset(_ *wayland.Surface, dx, dy int32) { | |||
| 781 | + | s.next.dx, s.next.dy = s.next.dx+dx, s.next.dy+dy | |||
| 782 | + | } | |||
| 783 | + | func (*surface) GetRelease(*wayland.Surface, uint32) {} | |||