Merge pull request 'feat: drag and drop between Wayland clients' (#61) from feat/data-dnd into main
20 files · +2759 -31
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 +594 -0
| @@ -0,0 +1,594 @@ | |||
| 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 actionAsk = uint32(wayland.DataDeviceManagerDndActionAsk) | |
| 28 | + | ||
| 29 | + | const dndAllActions = uint32(wayland.DataDeviceManagerDndActionCopy | wayland.DataDeviceManagerDndActionMove | wayland.DataDeviceManagerDndActionAsk) | |
| 30 | + | ||
| 31 | + | // dndState is the drag in progress. | |
| 32 | + | type dndState struct { | |
| 33 | + | // src is the source; nil for a drag internal to the origin client. | |
| 34 | + | src *clipSource | |
| 35 | + | client server.Client | |
| 36 | + | // icon is the icon surface (nil: none or destroyed); iconID is the | |
| 37 | + | // window ID core knows it by (0: none). | |
| 38 | + | icon *surface | |
| 39 | + | iconID ports.WindowID | |
| 40 | + | // iconX, iconY is the icon's top left corner relative to the cursor: | |
| 41 | + | // the offsets of its commits add up. iconSent is the last geometry sent. | |
| 42 | + | iconX, iconY int | |
| 43 | + | iconSent ports.DataDragIcon | |
| 44 | + | target *dndTarget | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | const ( | |
| 48 | + | targetActive = iota | |
| 49 | + | targetDropped | |
| 50 | + | targetDone | |
| 51 | + | ) | |
| 52 | + | ||
| 53 | + | // dndTarget is the surface a drag entered. Its offers (one per data device | |
| 54 | + | // of the client) share the negotiation. After the drop it lives on until | |
| 55 | + | // the target finishes or destroys its offers. | |
| 56 | + | type dndTarget struct { | |
| 57 | + | d *dndState | |
| 58 | + | surface *wayland.Surface | |
| 59 | + | devices []*wayland.DataDevice | |
| 60 | + | offers []*dndOffer | |
| 61 | + | // accepted is the MIME type the target accepted; action the action | |
| 62 | + | // chosen for the source. | |
| 63 | + | accepted string | |
| 64 | + | action uint32 | |
| 65 | + | state int | |
| 66 | + | // legacy: no device speaks wl_data_offer v3: the target copies and | |
| 67 | + | // nothing finishes it; destroying its offers does. | |
| 68 | + | legacy bool | |
| 69 | + | // ask: the drop came with the ask action; the target picks the final | |
| 70 | + | // one after it. | |
| 71 | + | ask bool | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // dndOffer is a wl_data_offer of a drag. | |
| 75 | + | type dndOffer struct { | |
| 76 | + | server *Server | |
| 77 | + | t *dndTarget | |
| 78 | + | res *wayland.DataOffer | |
| 79 | + | // actions and preferred are the target's set_actions; sent is the last | |
| 80 | + | // action told to this offer. | |
| 81 | + | actions, preferred, sent uint32 | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // srcActions is what the source allows; a source that never said (a | |
| 85 | + | // pre-v3 client) only copies. | |
| 86 | + | func (src *clipSource) srcActions() uint32 { | |
| 87 | + | if src.actionsSet { | |
| 88 | + | return src.actions | |
| 89 | + | } | |
| 90 | + | return uint32(wayland.DataDeviceManagerDndActionCopy) | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | // chooseAction picks the action like wlroots: the preferred one if both | |
| 94 | + | // allow it, else the first of copy, move, ask. | |
| 95 | + | func chooseAction(avail, preferred uint32) uint32 { | |
| 96 | + | if avail == 0 { | |
| 97 | + | return 0 | |
| 98 | + | } | |
| 99 | + | if preferred&avail != 0 { | |
| 100 | + | return preferred | |
| 101 | + | } | |
| 102 | + | for _, a := range [...]uint32{uint32(wayland.DataDeviceManagerDndActionCopy), uint32(wayland.DataDeviceManagerDndActionMove), uint32(wayland.DataDeviceManagerDndActionAsk)} { | |
| 103 | + | if avail&a != 0 { | |
| 104 | + | return a | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | return 0 | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | // addDataDevice tracks a client's data devices, the drag targets. | |
| 111 | + | func (s *Server) addDataDevice(c server.Client, dev *wayland.DataDevice) { | |
| 112 | + | if s.dataDevices == nil { | |
| 113 | + | s.dataDevices = map[server.Client][]*wayland.DataDevice{} | |
| 114 | + | } | |
| 115 | + | s.dataDevices[c] = append(s.dataDevices[c], dev) | |
| 116 | + | dev.OnDestroy = func() { | |
| 117 | + | // Copy on write: a drag may be iterating the old list. | |
| 118 | + | list := slices.DeleteFunc(slices.Clone(s.dataDevices[c]), func(x *wayland.DataDevice) bool { return x == dev }) | |
| 119 | + | if len(list) == 0 { | |
| 120 | + | delete(s.dataDevices, c) | |
| 121 | + | // An internal drag has nobody left to tell. | |
| 122 | + | if d := s.dnd; d != nil && d.client == c && d.src == nil { | |
| 123 | + | s.dndEnd(d, false, true) | |
| 124 | + | } | |
| 125 | + | } else { | |
| 126 | + | s.dataDevices[c] = list | |
| 127 | + | } | |
| 128 | + | } | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // startDrag handles wl_data_device.start_drag. | |
| 132 | + | func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, origin, icon *wayland.Surface, serial uint32) { | |
| 133 | + | var cs *clipSource | |
| 134 | + | if srcRes != nil { | |
| 135 | + | if cs = s.dataSources[srcRes.Resource]; cs == nil { | |
| 136 | + | return | |
| 137 | + | } | |
| 138 | + | } | |
| 139 | + | var iconSurf *surface | |
| 140 | + | if icon != nil { | |
| 141 | + | if iconSurf = s.surfaces[icon.Resource]; iconSurf == nil { | |
| 142 | + | return | |
| 143 | + | } | |
| 144 | + | if iconSurf.kind != roleNone && iconSurf.kind != roleDragIcon { | |
| 145 | + | r.PostError(uint32(wayland.DataDeviceErrorRole), "icon surface already has another role") | |
| 146 | + | return | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | client := r.Client() | |
| 150 | + | valid := !s.protected() && s.dnd == nil && origin != nil && origin.Client() == client && | |
| 151 | + | s.validGrab(origin, serial) | |
| 152 | + | if cs != nil && (cs.used || !valid) { | |
| 153 | + | // A used source is the selection, a drag source or already | |
| 154 | + | // cancelled: only a source nobody holds is cancelled again. | |
| 155 | + | if !s.sourceBusy(cs) { | |
| 156 | + | cs.used = true | |
| 157 | + | if cs.alive() { | |
| 158 | + | cs.cancel() | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | return | |
| 162 | + | } | |
| 163 | + | if !valid { | |
| 164 | + | return | |
| 165 | + | } | |
| 166 | + | d := &dndState{src: cs, client: client, icon: iconSurf} | |
| 167 | + | if cs != nil { | |
| 168 | + | cs.used = true | |
| 169 | + | } | |
| 170 | + | if iconSurf != nil { | |
| 171 | + | d.iconID = s.nextWindow | |
| 172 | + | s.nextWindow++ | |
| 173 | + | iconSurf.kind = roleDragIcon | |
| 174 | + | iconSurf.role = func(bool) { s.dndIconRole(d, iconSurf) } | |
| 175 | + | } | |
| 176 | + | s.dnd = d | |
| 177 | + | // Core consumes the releases of a drag: the grab ends here. | |
| 178 | + | button := s.seat.grabButton | |
| 179 | + | s.seat.grabSerial, s.seat.grabButton = 0, 0 | |
| 180 | + | s.log.Info().Uint64("icon", uint64(d.iconID)).Bool("source", cs != nil).Msg("data drag started") | |
| 181 | + | s.emit(ports.DataDragStarted{Button: button, Icon: d.iconID}) | |
| 182 | + | if iconSurf != nil && iconSurf.has { | |
| 183 | + | // The icon was committed before the drag: show what it holds. | |
| 184 | + | iconSurf.committed = damage{full: true} | |
| 185 | + | iconSurf.redraw() | |
| 186 | + | s.dndIconSend(d) | |
| 187 | + | } | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // validGrab reports whether serial is the press of the implicit grab still | |
| 191 | + | // held, on the surface with pointer focus (or a subsurface of its tree). | |
| 192 | + | func (s *Server) validGrab(origin *wayland.Surface, serial uint32) bool { | |
| 193 | + | if serial == 0 || serial != s.seat.grabSerial { | |
| 194 | + | return false | |
| 195 | + | } | |
| 196 | + | focus, _, _, _ := s.pointerSurface(s.seat.pointerFocus, 0, 0) | |
| 197 | + | if focus == nil { | |
| 198 | + | return false | |
| 199 | + | } | |
| 200 | + | o, f := s.surfaces[origin.Resource], s.surfaces[focus.Resource] | |
| 201 | + | return o != nil && f != nil && o.root() == f.root() | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | // sourceBusy reports whether src is the selection, the source of the drag | |
| 205 | + | // or of a drop waiting for its finish. | |
| 206 | + | func (s *Server) sourceBusy(src *clipSource) bool { | |
| 207 | + | if s.selections[selClipboard] == src || s.dnd != nil && s.dnd.src == src { | |
| 208 | + | return true | |
| 209 | + | } | |
| 210 | + | return slices.ContainsFunc(s.dndDrops, func(t *dndTarget) bool { return t.d.src == src }) | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | // dndIconRole runs on each commit of the icon surface, and on its | |
| 214 | + | // destruction. | |
| 215 | + | func (s *Server) dndIconRole(d *dndState, icon *surface) { | |
| 216 | + | if !icon.destroyed || d.icon != icon { | |
| 217 | + | return | |
| 218 | + | } | |
| 219 | + | // The drag goes on without icon. | |
| 220 | + | id := d.iconID | |
| 221 | + | d.icon, d.iconID = nil, 0 | |
| 222 | + | s.emit(ports.DataDragIcon{ID: id}) | |
| 223 | + | s.emitContent(ports.SurfaceContent{ID: id}, damage{full: true}) | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | // dndIconCommit follows a commit of the active icon: its offsets add up | |
| 227 | + | // and its geometry goes to core. | |
| 228 | + | func (s *Server) dndIconCommit(icon *surface, dx, dy int32) { | |
| 229 | + | d := s.dnd | |
| 230 | + | if d == nil || d.icon != icon { | |
| 231 | + | return | |
| 232 | + | } | |
| 233 | + | d.iconX += int(dx) | |
| 234 | + | d.iconY += int(dy) | |
| 235 | + | s.dndIconSend(d) | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | func (s *Server) dndIconSend(d *dndState) { | |
| 239 | + | ic := ports.DataDragIcon{ID: d.iconID, X: d.iconX, Y: d.iconY} | |
| 240 | + | if d.icon != nil && d.icon.has { | |
| 241 | + | ic.W, ic.H = d.icon.content.LogicalW, d.icon.content.LogicalH | |
| 242 | + | } | |
| 243 | + | if ic != d.iconSent { | |
| 244 | + | d.iconSent = ic | |
| 245 | + | s.emit(ic) | |
| 246 | + | } | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | // applyDrag applies the commands core sends during a drag. | |
| 250 | + | func (s *Server) applyDrag(cmd ports.ClientCommand) { | |
| 251 | + | d := s.dnd | |
| 252 | + | if d == nil { | |
| 253 | + | return | |
| 254 | + | } | |
| 255 | + | switch c := cmd.(type) { | |
| 256 | + | case ports.DataDragCancel: | |
| 257 | + | s.dndEnd(d, true, false) | |
| 258 | + | case ports.DataDragDrop: | |
| 259 | + | s.dndDrop(d) | |
| 260 | + | case ports.DataDragFocus: | |
| 261 | + | s.dndFocus(d, c) | |
| 262 | + | case ports.DataDragMotion: | |
| 263 | + | if t := d.target; t != nil && !s.protected() { | |
| 264 | + | surf, _, x, y := s.pointerSurface(c.ID, c.X, c.Y) | |
| 265 | + | if surf == nil || surf.Resource != t.surface.Resource { | |
| 266 | + | return | |
| 267 | + | } | |
| 268 | + | for _, dev := range t.devices { | |
| 269 | + | if dev.Resource.Alive() { | |
| 270 | + | dev.SendMotion(wireMsec(c.Time), server.FixedFromFloat(x), server.FixedFromFloat(y)) | |
| 271 | + | } | |
| 272 | + | } | |
| 273 | + | } | |
| 274 | + | } | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | // dndFocus moves the drag to the surface core names: the old target gets | |
| 278 | + | // leave, the new one enter with an offer. | |
| 279 | + | func (s *Server) dndFocus(d *dndState, c ports.DataDragFocus) { | |
| 280 | + | s.dndLeave(d) | |
| 281 | + | if c.ID == 0 || s.protected() { | |
| 282 | + | return | |
| 283 | + | } | |
| 284 | + | surf, _, x, y := s.pointerSurface(c.ID, c.X, c.Y) | |
| 285 | + | if surf == nil { | |
| 286 | + | return | |
| 287 | + | } | |
| 288 | + | client := surf.Client() | |
| 289 | + | // Without a source the drag is internal to the origin client. | |
| 290 | + | if d.src == nil && client != d.client { | |
| 291 | + | return | |
| 292 | + | } | |
| 293 | + | devices := s.dataDevices[client] | |
| 294 | + | if len(devices) == 0 { | |
| 295 | + | return | |
| 296 | + | } | |
| 297 | + | t := &dndTarget{d: d, surface: surf, devices: devices, legacy: true} | |
| 298 | + | d.target = t | |
| 299 | + | for _, dev := range devices { | |
| 300 | + | if !dev.Resource.Alive() { | |
| 301 | + | continue | |
| 302 | + | } | |
| 303 | + | if dev.Version() >= 3 { | |
| 304 | + | t.legacy = false | |
| 305 | + | } | |
| 306 | + | var offer *wayland.DataOffer | |
| 307 | + | if d.src != nil { | |
| 308 | + | o := &dndOffer{server: s, t: t} | |
| 309 | + | res, err := wayland.NewDataOffer(dev.Client(), dev.Version(), 0, o) | |
| 310 | + | if err != nil { | |
| 311 | + | continue | |
| 312 | + | } | |
| 313 | + | o.res = res | |
| 314 | + | res.OnDestroy = func() { s.dndOfferGone(o) } | |
| 315 | + | t.offers = append(t.offers, o) | |
| 316 | + | dev.SendDataOffer(res) | |
| 317 | + | for _, mime := range d.src.mimes { | |
| 318 | + | res.SendOffer(mime) | |
| 319 | + | } | |
| 320 | + | if dev.Version() >= 3 { | |
| 321 | + | res.SendSourceActions(d.src.srcActions()) | |
| 322 | + | } | |
| 323 | + | offer = res | |
| 324 | + | } | |
| 325 | + | s.serial++ | |
| 326 | + | dev.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y), offer) | |
| 327 | + | } | |
| 328 | + | } | |
| 329 | + | ||
| 330 | + | // dndLeave sends leave to the target of the drag and forgets it: its | |
| 331 | + | // offers become inert. | |
| 332 | + | func (s *Server) dndLeave(d *dndState) { | |
| 333 | + | t := d.target | |
| 334 | + | if t == nil { | |
| 335 | + | return | |
| 336 | + | } | |
| 337 | + | d.target = nil | |
| 338 | + | t.state = targetDone | |
| 339 | + | s.dndSendLeave(t) | |
| 340 | + | s.dndResetSource(t) | |
| 341 | + | } | |
| 342 | + | ||
| 343 | + | func (s *Server) dndSendLeave(t *dndTarget) { | |
| 344 | + | for _, dev := range t.devices { | |
| 345 | + | if dev.Resource.Alive() { | |
| 346 | + | dev.SendLeave() | |
| 347 | + | } | |
| 348 | + | } | |
| 349 | + | } | |
| 350 | + | ||
| 351 | + | // dndResetSource tells the source nothing accepts or acts any more. | |
| 352 | + | func (s *Server) dndResetSource(t *dndTarget) { | |
| 353 | + | src := t.d.src | |
| 354 | + | if src == nil || !src.alive() { | |
| 355 | + | return | |
| 356 | + | } | |
| 357 | + | if t.accepted != "" { | |
| 358 | + | src.res.SendTarget("") | |
| 359 | + | } | |
| 360 | + | if t.action != 0 && src.res.Version() >= 3 { | |
| 361 | + | src.res.SendAction(0) | |
| 362 | + | } | |
| 363 | + | t.accepted, t.action = "", 0 | |
| 364 | + | } | |
| 365 | + | ||
| 366 | + | // dndDrop handles the release of the drag button: a target that accepted | |
| 367 | + | // and agreed on an action gets drop, then leave (its offers stay usable until | |
| 368 | + | // finish); else the drag is cancelled. | |
| 369 | + | func (s *Server) dndDrop(d *dndState) { | |
| 370 | + | t := d.target | |
| 371 | + | if t == nil { | |
| 372 | + | s.dndEnd(d, true, false) | |
| 373 | + | return | |
| 374 | + | } | |
| 375 | + | if d.src == nil { | |
| 376 | + | // Internal drag: the origin client decides alone. | |
| 377 | + | d.target = nil | |
| 378 | + | t.state = targetDone | |
| 379 | + | s.dndSendDrop(t) | |
| 380 | + | s.dndSendLeave(t) | |
| 381 | + | s.dndEnd(d, false, false) | |
| 382 | + | return | |
| 383 | + | } | |
| 384 | + | if t.legacy { | |
| 385 | + | // A pre-v3 target cannot negotiate: it copies. | |
| 386 | + | action := chooseAction(uint32(wayland.DataDeviceManagerDndActionCopy)&d.src.srcActions(), 0) | |
| 387 | + | if action != t.action { | |
| 388 | + | t.action = action | |
| 389 | + | if action != 0 && d.src.alive() && d.src.res.Version() >= 3 { | |
| 390 | + | d.src.res.SendAction(action) | |
| 391 | + | } | |
| 392 | + | } | |
| 393 | + | } | |
| 394 | + | if t.accepted == "" || t.action == 0 { | |
| 395 | + | s.dndEnd(d, true, false) | |
| 396 | + | return | |
| 397 | + | } | |
| 398 | + | d.target = nil | |
| 399 | + | t.state = targetDropped | |
| 400 | + | t.ask = t.action == actionAsk | |
| 401 | + | s.dndDrops = append(s.dndDrops, t) | |
| 402 | + | s.dndSendDrop(t) | |
| 403 | + | s.dndSendLeave(t) | |
| 404 | + | if d.src.alive() && d.src.res.Version() >= 3 { | |
| 405 | + | d.src.res.SendDndDropPerformed() | |
| 406 | + | } | |
| 407 | + | s.log.Info().Str("mime", t.accepted).Uint32("action", t.action).Msg("data drag dropped") | |
| 408 | + | s.dndEnd(d, false, false) | |
| 409 | + | } | |
| 410 | + | ||
| 411 | + | func (s *Server) dndSendDrop(t *dndTarget) { | |
| 412 | + | for _, dev := range t.devices { | |
| 413 | + | if dev.Resource.Alive() { | |
| 414 | + | dev.SendDrop() | |
| 415 | + | } | |
| 416 | + | } | |
| 417 | + | } | |
| 418 | + | ||
| 419 | + | // dndEnd ends the drag d: the target gets leave, the source cancelled if | |
| 420 | + | // cancel (it is not once dropped), the icon goes. ended tells core about an | |
| 421 | + | // end it did not cause. | |
| 422 | + | func (s *Server) dndEnd(d *dndState, cancel, ended bool) { | |
| 423 | + | s.dndLeave(d) | |
| 424 | + | if cancel && d.src != nil && d.src.alive() { | |
| 425 | + | d.src.cancel() | |
| 426 | + | } | |
| 427 | + | if s.dnd == d { | |
| 428 | + | s.dnd = nil | |
| 429 | + | } | |
| 430 | + | if d.icon != nil { | |
| 431 | + | d.icon.role = nil | |
| 432 | + | d.icon = nil | |
| 433 | + | } | |
| 434 | + | if d.iconID != 0 { | |
| 435 | + | s.emitContent(ports.SurfaceContent{ID: d.iconID}, damage{full: true}) | |
| 436 | + | d.iconID = 0 | |
| 437 | + | } | |
| 438 | + | if ended { | |
| 439 | + | s.log.Info().Msg("data drag ended") | |
| 440 | + | s.emit(ports.DataDragEnded{}) | |
| 441 | + | } | |
| 442 | + | } | |
| 443 | + | ||
| 444 | + | // dndSourceGone ends the drags of a destroyed source. | |
| 445 | + | func (s *Server) dndSourceGone(src *clipSource) { | |
| 446 | + | if d := s.dnd; d != nil && d.src == src { | |
| 447 | + | s.dndEnd(d, false, true) | |
| 448 | + | } | |
| 449 | + | for _, t := range slices.Clone(s.dndDrops) { | |
| 450 | + | if t.d.src == src { | |
| 451 | + | s.dndDropDone(t) | |
| 452 | + | } | |
| 453 | + | } | |
| 454 | + | } | |
| 455 | + | ||
| 456 | + | // dndDropDone forgets a dropped target. | |
| 457 | + | func (s *Server) dndDropDone(t *dndTarget) { | |
| 458 | + | t.state = targetDone | |
| 459 | + | s.dndDrops = slices.DeleteFunc(s.dndDrops, func(x *dndTarget) bool { return x == t }) | |
| 460 | + | } | |
| 461 | + | ||
| 462 | + | // dndSurfaceGone clears the drag target when its surface is destroyed. | |
| 463 | + | func (s *Server) dndSurfaceGone(surf *surface) { | |
| 464 | + | if d := s.dnd; d != nil && d.target != nil && surf.wl != nil && d.target.surface.Resource == surf.wl.Resource { | |
| 465 | + | s.dndLeave(d) | |
| 466 | + | } | |
| 467 | + | } | |
| 468 | + | ||
| 469 | + | // dndCancelAll cancels the drag and the drops waiting for their finish: the | |
| 470 | + | // session is protected. | |
| 471 | + | func (s *Server) dndCancelAll() { | |
| 472 | + | if d := s.dnd; d != nil { | |
| 473 | + | s.dndEnd(d, true, false) | |
| 474 | + | } | |
| 475 | + | for _, t := range slices.Clone(s.dndDrops) { | |
| 476 | + | if src := t.d.src; src != nil && src.alive() { | |
| 477 | + | src.cancel() | |
| 478 | + | } | |
| 479 | + | s.dndDropDone(t) | |
| 480 | + | } | |
| 481 | + | } | |
| 482 | + | ||
| 483 | + | // dndOfferGone runs when an offer of a drag is destroyed. With none left on | |
| 484 | + | // a live target, nothing is accepted any more. With none left on a dropped | |
| 485 | + | // one the transfer is over: a pre-v3 target has finished, any other | |
| 486 | + | // cancelled it. | |
| 487 | + | func (s *Server) dndOfferGone(o *dndOffer) { | |
| 488 | + | t := o.t | |
| 489 | + | t.offers = slices.DeleteFunc(t.offers, func(x *dndOffer) bool { return x == o }) | |
| 490 | + | if len(t.offers) != 0 { | |
| 491 | + | return | |
| 492 | + | } | |
| 493 | + | switch t.state { | |
| 494 | + | case targetActive: | |
| 495 | + | s.dndResetSource(t) | |
| 496 | + | case targetDropped: | |
| 497 | + | if src := t.d.src; src != nil && src.alive() { | |
| 498 | + | if !t.legacy { | |
| 499 | + | src.cancel() | |
| 500 | + | } else if src.res.Version() >= 3 { | |
| 501 | + | src.res.SendDndFinished() | |
| 502 | + | } | |
| 503 | + | } | |
| 504 | + | s.dndDropDone(t) | |
| 505 | + | } | |
| 506 | + | } | |
| 507 | + | ||
| 508 | + | // update recomputes the action after the target or the source changed | |
| 509 | + | // something, and tells both when it changed. After an ask drop the choice is | |
| 510 | + | // silent until finish. | |
| 511 | + | func (o *dndOffer) update() { | |
| 512 | + | t, src := o.t, o.t.d.src | |
| 513 | + | if t.state == targetDone || t.state == targetDropped && !t.ask || src == nil || !src.alive() { | |
| 514 | + | return | |
| 515 | + | } | |
| 516 | + | action := chooseAction(o.actions&src.srcActions(), o.preferred) | |
| 517 | + | if action != t.action { | |
| 518 | + | t.action = action | |
| 519 | + | if t.state == targetActive && src.res.Version() >= 3 { | |
| 520 | + | src.res.SendAction(action) | |
| 521 | + | } | |
| 522 | + | } | |
| 523 | + | if t.state == targetActive && action != o.sent { | |
| 524 | + | o.sent = action | |
| 525 | + | if o.res.Version() >= 3 { | |
| 526 | + | o.res.SendAction(action) | |
| 527 | + | } | |
| 528 | + | } | |
| 529 | + | } | |
| 530 | + | ||
| 531 | + | func (o *dndOffer) Accept(_ *wayland.DataOffer, _ uint32, mime string) { | |
| 532 | + | t := o.t | |
| 533 | + | if t.state != targetActive && !(t.state == targetDropped && t.ask) || !t.d.src.alive() { | |
| 534 | + | return | |
| 535 | + | } | |
| 536 | + | t.accepted = mime | |
| 537 | + | t.d.src.res.SendTarget(mime) | |
| 538 | + | } | |
| 539 | + | ||
| 540 | + | func (o *dndOffer) Receive(_ *wayland.DataOffer, mime string, fd int) { | |
| 541 | + | defer unix.Close(fd) | |
| 542 | + | src := o.t.d.src | |
| 543 | + | if o.server.protected() || o.t.state == targetDone || !src.alive() { | |
| 544 | + | return | |
| 545 | + | } | |
| 546 | + | if slices.Contains(src.mimes, mime) { | |
| 547 | + | src.send(mime, fd) | |
| 548 | + | } | |
| 549 | + | } | |
| 550 | + | ||
| 551 | + | func (*dndOffer) Destroy(*wayland.DataOffer) {} | |
| 552 | + | ||
| 553 | + | func (o *dndOffer) Finish(r *wayland.DataOffer) { | |
| 554 | + | t := o.t | |
| 555 | + | switch t.state { | |
| 556 | + | case targetDone: | |
| 557 | + | return | |
| 558 | + | case targetActive: | |
| 559 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish before the drop") | |
| 560 | + | return | |
| 561 | + | } | |
| 562 | + | if t.action == 0 || t.action == actionAsk { | |
| 563 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without a final action") | |
| 564 | + | return | |
| 565 | + | } | |
| 566 | + | if t.accepted == "" { | |
| 567 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without an accepted MIME type") | |
| 568 | + | return | |
| 569 | + | } | |
| 570 | + | o.server.dndDropDone(t) | |
| 571 | + | if src := t.d.src; src.alive() && src.res.Version() >= 3 { | |
| 572 | + | if t.ask { | |
| 573 | + | src.res.SendAction(t.action) | |
| 574 | + | } | |
| 575 | + | src.res.SendDndFinished() | |
| 576 | + | } | |
| 577 | + | } | |
| 578 | + | ||
| 579 | + | func (o *dndOffer) SetActions(r *wayland.DataOffer, actions, preferred uint32) { | |
| 580 | + | if actions&^dndAllActions != 0 { | |
| 581 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidActionMask), "invalid action mask") | |
| 582 | + | return | |
| 583 | + | } | |
| 584 | + | if preferred != 0 && (preferred&(preferred-1) != 0 || preferred&actions == 0) { | |
| 585 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "invalid preferred action") | |
| 586 | + | return | |
| 587 | + | } | |
| 588 | + | if t := o.t; t.state == targetDropped && t.ask && preferred != 0 && t.d.src != nil && preferred&t.d.src.srcActions() == 0 { | |
| 589 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "final action not allowed by the source") | |
| 590 | + | return | |
| 591 | + | } | |
| 592 | + | o.actions, o.preferred = actions, preferred | |
| 593 | + | o.update() | |
| 594 | + | } | |
| @@ -0,0 +1,594 @@ | |||||
| 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 actionAsk = uint32(wayland.DataDeviceManagerDndActionAsk) | |||
| 28 | + | ||||
| 29 | + | const dndAllActions = uint32(wayland.DataDeviceManagerDndActionCopy | wayland.DataDeviceManagerDndActionMove | wayland.DataDeviceManagerDndActionAsk) | |||
| 30 | + | ||||
| 31 | + | // dndState is the drag in progress. | |||
| 32 | + | type dndState struct { | |||
| 33 | + | // src is the source; nil for a drag internal to the origin client. | |||
| 34 | + | src *clipSource | |||
| 35 | + | client server.Client | |||
| 36 | + | // icon is the icon surface (nil: none or destroyed); iconID is the | |||
| 37 | + | // window ID core knows it by (0: none). | |||
| 38 | + | icon *surface | |||
| 39 | + | iconID ports.WindowID | |||
| 40 | + | // iconX, iconY is the icon's top left corner relative to the cursor: | |||
| 41 | + | // the offsets of its commits add up. iconSent is the last geometry sent. | |||
| 42 | + | iconX, iconY int | |||
| 43 | + | iconSent ports.DataDragIcon | |||
| 44 | + | target *dndTarget | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | const ( | |||
| 48 | + | targetActive = iota | |||
| 49 | + | targetDropped | |||
| 50 | + | targetDone | |||
| 51 | + | ) | |||
| 52 | + | ||||
| 53 | + | // dndTarget is the surface a drag entered. Its offers (one per data device | |||
| 54 | + | // of the client) share the negotiation. After the drop it lives on until | |||
| 55 | + | // the target finishes or destroys its offers. | |||
| 56 | + | type dndTarget struct { | |||
| 57 | + | d *dndState | |||
| 58 | + | surface *wayland.Surface | |||
| 59 | + | devices []*wayland.DataDevice | |||
| 60 | + | offers []*dndOffer | |||
| 61 | + | // accepted is the MIME type the target accepted; action the action | |||
| 62 | + | // chosen for the source. | |||
| 63 | + | accepted string | |||
| 64 | + | action uint32 | |||
| 65 | + | state int | |||
| 66 | + | // legacy: no device speaks wl_data_offer v3: the target copies and | |||
| 67 | + | // nothing finishes it; destroying its offers does. | |||
| 68 | + | legacy bool | |||
| 69 | + | // ask: the drop came with the ask action; the target picks the final | |||
| 70 | + | // one after it. | |||
| 71 | + | ask bool | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | // dndOffer is a wl_data_offer of a drag. | |||
| 75 | + | type dndOffer struct { | |||
| 76 | + | server *Server | |||
| 77 | + | t *dndTarget | |||
| 78 | + | res *wayland.DataOffer | |||
| 79 | + | // actions and preferred are the target's set_actions; sent is the last | |||
| 80 | + | // action told to this offer. | |||
| 81 | + | actions, preferred, sent uint32 | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | // srcActions is what the source allows; a source that never said (a | |||
| 85 | + | // pre-v3 client) only copies. | |||
| 86 | + | func (src *clipSource) srcActions() uint32 { | |||
| 87 | + | if src.actionsSet { | |||
| 88 | + | return src.actions | |||
| 89 | + | } | |||
| 90 | + | return uint32(wayland.DataDeviceManagerDndActionCopy) | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | // chooseAction picks the action like wlroots: the preferred one if both | |||
| 94 | + | // allow it, else the first of copy, move, ask. | |||
| 95 | + | func chooseAction(avail, preferred uint32) uint32 { | |||
| 96 | + | if avail == 0 { | |||
| 97 | + | return 0 | |||
| 98 | + | } | |||
| 99 | + | if preferred&avail != 0 { | |||
| 100 | + | return preferred | |||
| 101 | + | } | |||
| 102 | + | for _, a := range [...]uint32{uint32(wayland.DataDeviceManagerDndActionCopy), uint32(wayland.DataDeviceManagerDndActionMove), uint32(wayland.DataDeviceManagerDndActionAsk)} { | |||
| 103 | + | if avail&a != 0 { | |||
| 104 | + | return a | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | return 0 | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | // addDataDevice tracks a client's data devices, the drag targets. | |||
| 111 | + | func (s *Server) addDataDevice(c server.Client, dev *wayland.DataDevice) { | |||
| 112 | + | if s.dataDevices == nil { | |||
| 113 | + | s.dataDevices = map[server.Client][]*wayland.DataDevice{} | |||
| 114 | + | } | |||
| 115 | + | s.dataDevices[c] = append(s.dataDevices[c], dev) | |||
| 116 | + | dev.OnDestroy = func() { | |||
| 117 | + | // Copy on write: a drag may be iterating the old list. | |||
| 118 | + | list := slices.DeleteFunc(slices.Clone(s.dataDevices[c]), func(x *wayland.DataDevice) bool { return x == dev }) | |||
| 119 | + | if len(list) == 0 { | |||
| 120 | + | delete(s.dataDevices, c) | |||
| 121 | + | // An internal drag has nobody left to tell. | |||
| 122 | + | if d := s.dnd; d != nil && d.client == c && d.src == nil { | |||
| 123 | + | s.dndEnd(d, false, true) | |||
| 124 | + | } | |||
| 125 | + | } else { | |||
| 126 | + | s.dataDevices[c] = list | |||
| 127 | + | } | |||
| 128 | + | } | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // startDrag handles wl_data_device.start_drag. | |||
| 132 | + | func (s *Server) startDrag(r *wayland.DataDevice, srcRes *wayland.DataSource, origin, icon *wayland.Surface, serial uint32) { | |||
| 133 | + | var cs *clipSource | |||
| 134 | + | if srcRes != nil { | |||
| 135 | + | if cs = s.dataSources[srcRes.Resource]; cs == nil { | |||
| 136 | + | return | |||
| 137 | + | } | |||
| 138 | + | } | |||
| 139 | + | var iconSurf *surface | |||
| 140 | + | if icon != nil { | |||
| 141 | + | if iconSurf = s.surfaces[icon.Resource]; iconSurf == nil { | |||
| 142 | + | return | |||
| 143 | + | } | |||
| 144 | + | if iconSurf.kind != roleNone && iconSurf.kind != roleDragIcon { | |||
| 145 | + | r.PostError(uint32(wayland.DataDeviceErrorRole), "icon surface already has another role") | |||
| 146 | + | return | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | client := r.Client() | |||
| 150 | + | valid := !s.protected() && s.dnd == nil && origin != nil && origin.Client() == client && | |||
| 151 | + | s.validGrab(origin, serial) | |||
| 152 | + | if cs != nil && (cs.used || !valid) { | |||
| 153 | + | // A used source is the selection, a drag source or already | |||
| 154 | + | // cancelled: only a source nobody holds is cancelled again. | |||
| 155 | + | if !s.sourceBusy(cs) { | |||
| 156 | + | cs.used = true | |||
| 157 | + | if cs.alive() { | |||
| 158 | + | cs.cancel() | |||
| 159 | + | } | |||
| 160 | + | } | |||
| 161 | + | return | |||
| 162 | + | } | |||
| 163 | + | if !valid { | |||
| 164 | + | return | |||
| 165 | + | } | |||
| 166 | + | d := &dndState{src: cs, client: client, icon: iconSurf} | |||
| 167 | + | if cs != nil { | |||
| 168 | + | cs.used = true | |||
| 169 | + | } | |||
| 170 | + | if iconSurf != nil { | |||
| 171 | + | d.iconID = s.nextWindow | |||
| 172 | + | s.nextWindow++ | |||
| 173 | + | iconSurf.kind = roleDragIcon | |||
| 174 | + | iconSurf.role = func(bool) { s.dndIconRole(d, iconSurf) } | |||
| 175 | + | } | |||
| 176 | + | s.dnd = d | |||
| 177 | + | // Core consumes the releases of a drag: the grab ends here. | |||
| 178 | + | button := s.seat.grabButton | |||
| 179 | + | s.seat.grabSerial, s.seat.grabButton = 0, 0 | |||
| 180 | + | s.log.Info().Uint64("icon", uint64(d.iconID)).Bool("source", cs != nil).Msg("data drag started") | |||
| 181 | + | s.emit(ports.DataDragStarted{Button: button, Icon: d.iconID}) | |||
| 182 | + | if iconSurf != nil && iconSurf.has { | |||
| 183 | + | // The icon was committed before the drag: show what it holds. | |||
| 184 | + | iconSurf.committed = damage{full: true} | |||
| 185 | + | iconSurf.redraw() | |||
| 186 | + | s.dndIconSend(d) | |||
| 187 | + | } | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | // validGrab reports whether serial is the press of the implicit grab still | |||
| 191 | + | // held, on the surface with pointer focus (or a subsurface of its tree). | |||
| 192 | + | func (s *Server) validGrab(origin *wayland.Surface, serial uint32) bool { | |||
| 193 | + | if serial == 0 || serial != s.seat.grabSerial { | |||
| 194 | + | return false | |||
| 195 | + | } | |||
| 196 | + | focus, _, _, _ := s.pointerSurface(s.seat.pointerFocus, 0, 0) | |||
| 197 | + | if focus == nil { | |||
| 198 | + | return false | |||
| 199 | + | } | |||
| 200 | + | o, f := s.surfaces[origin.Resource], s.surfaces[focus.Resource] | |||
| 201 | + | return o != nil && f != nil && o.root() == f.root() | |||
| 202 | + | } | |||
| 203 | + | ||||
| 204 | + | // sourceBusy reports whether src is the selection, the source of the drag | |||
| 205 | + | // or of a drop waiting for its finish. | |||
| 206 | + | func (s *Server) sourceBusy(src *clipSource) bool { | |||
| 207 | + | if s.selections[selClipboard] == src || s.dnd != nil && s.dnd.src == src { | |||
| 208 | + | return true | |||
| 209 | + | } | |||
| 210 | + | return slices.ContainsFunc(s.dndDrops, func(t *dndTarget) bool { return t.d.src == src }) | |||
| 211 | + | } | |||
| 212 | + | ||||
| 213 | + | // dndIconRole runs on each commit of the icon surface, and on its | |||
| 214 | + | // destruction. | |||
| 215 | + | func (s *Server) dndIconRole(d *dndState, icon *surface) { | |||
| 216 | + | if !icon.destroyed || d.icon != icon { | |||
| 217 | + | return | |||
| 218 | + | } | |||
| 219 | + | // The drag goes on without icon. | |||
| 220 | + | id := d.iconID | |||
| 221 | + | d.icon, d.iconID = nil, 0 | |||
| 222 | + | s.emit(ports.DataDragIcon{ID: id}) | |||
| 223 | + | s.emitContent(ports.SurfaceContent{ID: id}, damage{full: true}) | |||
| 224 | + | } | |||
| 225 | + | ||||
| 226 | + | // dndIconCommit follows a commit of the active icon: its offsets add up | |||
| 227 | + | // and its geometry goes to core. | |||
| 228 | + | func (s *Server) dndIconCommit(icon *surface, dx, dy int32) { | |||
| 229 | + | d := s.dnd | |||
| 230 | + | if d == nil || d.icon != icon { | |||
| 231 | + | return | |||
| 232 | + | } | |||
| 233 | + | d.iconX += int(dx) | |||
| 234 | + | d.iconY += int(dy) | |||
| 235 | + | s.dndIconSend(d) | |||
| 236 | + | } | |||
| 237 | + | ||||
| 238 | + | func (s *Server) dndIconSend(d *dndState) { | |||
| 239 | + | ic := ports.DataDragIcon{ID: d.iconID, X: d.iconX, Y: d.iconY} | |||
| 240 | + | if d.icon != nil && d.icon.has { | |||
| 241 | + | ic.W, ic.H = d.icon.content.LogicalW, d.icon.content.LogicalH | |||
| 242 | + | } | |||
| 243 | + | if ic != d.iconSent { | |||
| 244 | + | d.iconSent = ic | |||
| 245 | + | s.emit(ic) | |||
| 246 | + | } | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | // applyDrag applies the commands core sends during a drag. | |||
| 250 | + | func (s *Server) applyDrag(cmd ports.ClientCommand) { | |||
| 251 | + | d := s.dnd | |||
| 252 | + | if d == nil { | |||
| 253 | + | return | |||
| 254 | + | } | |||
| 255 | + | switch c := cmd.(type) { | |||
| 256 | + | case ports.DataDragCancel: | |||
| 257 | + | s.dndEnd(d, true, false) | |||
| 258 | + | case ports.DataDragDrop: | |||
| 259 | + | s.dndDrop(d) | |||
| 260 | + | case ports.DataDragFocus: | |||
| 261 | + | s.dndFocus(d, c) | |||
| 262 | + | case ports.DataDragMotion: | |||
| 263 | + | if t := d.target; t != nil && !s.protected() { | |||
| 264 | + | surf, _, x, y := s.pointerSurface(c.ID, c.X, c.Y) | |||
| 265 | + | if surf == nil || surf.Resource != t.surface.Resource { | |||
| 266 | + | return | |||
| 267 | + | } | |||
| 268 | + | for _, dev := range t.devices { | |||
| 269 | + | if dev.Resource.Alive() { | |||
| 270 | + | dev.SendMotion(wireMsec(c.Time), server.FixedFromFloat(x), server.FixedFromFloat(y)) | |||
| 271 | + | } | |||
| 272 | + | } | |||
| 273 | + | } | |||
| 274 | + | } | |||
| 275 | + | } | |||
| 276 | + | ||||
| 277 | + | // dndFocus moves the drag to the surface core names: the old target gets | |||
| 278 | + | // leave, the new one enter with an offer. | |||
| 279 | + | func (s *Server) dndFocus(d *dndState, c ports.DataDragFocus) { | |||
| 280 | + | s.dndLeave(d) | |||
| 281 | + | if c.ID == 0 || s.protected() { | |||
| 282 | + | return | |||
| 283 | + | } | |||
| 284 | + | surf, _, x, y := s.pointerSurface(c.ID, c.X, c.Y) | |||
| 285 | + | if surf == nil { | |||
| 286 | + | return | |||
| 287 | + | } | |||
| 288 | + | client := surf.Client() | |||
| 289 | + | // Without a source the drag is internal to the origin client. | |||
| 290 | + | if d.src == nil && client != d.client { | |||
| 291 | + | return | |||
| 292 | + | } | |||
| 293 | + | devices := s.dataDevices[client] | |||
| 294 | + | if len(devices) == 0 { | |||
| 295 | + | return | |||
| 296 | + | } | |||
| 297 | + | t := &dndTarget{d: d, surface: surf, devices: devices, legacy: true} | |||
| 298 | + | d.target = t | |||
| 299 | + | for _, dev := range devices { | |||
| 300 | + | if !dev.Resource.Alive() { | |||
| 301 | + | continue | |||
| 302 | + | } | |||
| 303 | + | if dev.Version() >= 3 { | |||
| 304 | + | t.legacy = false | |||
| 305 | + | } | |||
| 306 | + | var offer *wayland.DataOffer | |||
| 307 | + | if d.src != nil { | |||
| 308 | + | o := &dndOffer{server: s, t: t} | |||
| 309 | + | res, err := wayland.NewDataOffer(dev.Client(), dev.Version(), 0, o) | |||
| 310 | + | if err != nil { | |||
| 311 | + | continue | |||
| 312 | + | } | |||
| 313 | + | o.res = res | |||
| 314 | + | res.OnDestroy = func() { s.dndOfferGone(o) } | |||
| 315 | + | t.offers = append(t.offers, o) | |||
| 316 | + | dev.SendDataOffer(res) | |||
| 317 | + | for _, mime := range d.src.mimes { | |||
| 318 | + | res.SendOffer(mime) | |||
| 319 | + | } | |||
| 320 | + | if dev.Version() >= 3 { | |||
| 321 | + | res.SendSourceActions(d.src.srcActions()) | |||
| 322 | + | } | |||
| 323 | + | offer = res | |||
| 324 | + | } | |||
| 325 | + | s.serial++ | |||
| 326 | + | dev.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y), offer) | |||
| 327 | + | } | |||
| 328 | + | } | |||
| 329 | + | ||||
| 330 | + | // dndLeave sends leave to the target of the drag and forgets it: its | |||
| 331 | + | // offers become inert. | |||
| 332 | + | func (s *Server) dndLeave(d *dndState) { | |||
| 333 | + | t := d.target | |||
| 334 | + | if t == nil { | |||
| 335 | + | return | |||
| 336 | + | } | |||
| 337 | + | d.target = nil | |||
| 338 | + | t.state = targetDone | |||
| 339 | + | s.dndSendLeave(t) | |||
| 340 | + | s.dndResetSource(t) | |||
| 341 | + | } | |||
| 342 | + | ||||
| 343 | + | func (s *Server) dndSendLeave(t *dndTarget) { | |||
| 344 | + | for _, dev := range t.devices { | |||
| 345 | + | if dev.Resource.Alive() { | |||
| 346 | + | dev.SendLeave() | |||
| 347 | + | } | |||
| 348 | + | } | |||
| 349 | + | } | |||
| 350 | + | ||||
| 351 | + | // dndResetSource tells the source nothing accepts or acts any more. | |||
| 352 | + | func (s *Server) dndResetSource(t *dndTarget) { | |||
| 353 | + | src := t.d.src | |||
| 354 | + | if src == nil || !src.alive() { | |||
| 355 | + | return | |||
| 356 | + | } | |||
| 357 | + | if t.accepted != "" { | |||
| 358 | + | src.res.SendTarget("") | |||
| 359 | + | } | |||
| 360 | + | if t.action != 0 && src.res.Version() >= 3 { | |||
| 361 | + | src.res.SendAction(0) | |||
| 362 | + | } | |||
| 363 | + | t.accepted, t.action = "", 0 | |||
| 364 | + | } | |||
| 365 | + | ||||
| 366 | + | // dndDrop handles the release of the drag button: a target that accepted | |||
| 367 | + | // and agreed on an action gets drop, then leave (its offers stay usable until | |||
| 368 | + | // finish); else the drag is cancelled. | |||
| 369 | + | func (s *Server) dndDrop(d *dndState) { | |||
| 370 | + | t := d.target | |||
| 371 | + | if t == nil { | |||
| 372 | + | s.dndEnd(d, true, false) | |||
| 373 | + | return | |||
| 374 | + | } | |||
| 375 | + | if d.src == nil { | |||
| 376 | + | // Internal drag: the origin client decides alone. | |||
| 377 | + | d.target = nil | |||
| 378 | + | t.state = targetDone | |||
| 379 | + | s.dndSendDrop(t) | |||
| 380 | + | s.dndSendLeave(t) | |||
| 381 | + | s.dndEnd(d, false, false) | |||
| 382 | + | return | |||
| 383 | + | } | |||
| 384 | + | if t.legacy { | |||
| 385 | + | // A pre-v3 target cannot negotiate: it copies. | |||
| 386 | + | action := chooseAction(uint32(wayland.DataDeviceManagerDndActionCopy)&d.src.srcActions(), 0) | |||
| 387 | + | if action != t.action { | |||
| 388 | + | t.action = action | |||
| 389 | + | if action != 0 && d.src.alive() && d.src.res.Version() >= 3 { | |||
| 390 | + | d.src.res.SendAction(action) | |||
| 391 | + | } | |||
| 392 | + | } | |||
| 393 | + | } | |||
| 394 | + | if t.accepted == "" || t.action == 0 { | |||
| 395 | + | s.dndEnd(d, true, false) | |||
| 396 | + | return | |||
| 397 | + | } | |||
| 398 | + | d.target = nil | |||
| 399 | + | t.state = targetDropped | |||
| 400 | + | t.ask = t.action == actionAsk | |||
| 401 | + | s.dndDrops = append(s.dndDrops, t) | |||
| 402 | + | s.dndSendDrop(t) | |||
| 403 | + | s.dndSendLeave(t) | |||
| 404 | + | if d.src.alive() && d.src.res.Version() >= 3 { | |||
| 405 | + | d.src.res.SendDndDropPerformed() | |||
| 406 | + | } | |||
| 407 | + | s.log.Info().Str("mime", t.accepted).Uint32("action", t.action).Msg("data drag dropped") | |||
| 408 | + | s.dndEnd(d, false, false) | |||
| 409 | + | } | |||
| 410 | + | ||||
| 411 | + | func (s *Server) dndSendDrop(t *dndTarget) { | |||
| 412 | + | for _, dev := range t.devices { | |||
| 413 | + | if dev.Resource.Alive() { | |||
| 414 | + | dev.SendDrop() | |||
| 415 | + | } | |||
| 416 | + | } | |||
| 417 | + | } | |||
| 418 | + | ||||
| 419 | + | // dndEnd ends the drag d: the target gets leave, the source cancelled if | |||
| 420 | + | // cancel (it is not once dropped), the icon goes. ended tells core about an | |||
| 421 | + | // end it did not cause. | |||
| 422 | + | func (s *Server) dndEnd(d *dndState, cancel, ended bool) { | |||
| 423 | + | s.dndLeave(d) | |||
| 424 | + | if cancel && d.src != nil && d.src.alive() { | |||
| 425 | + | d.src.cancel() | |||
| 426 | + | } | |||
| 427 | + | if s.dnd == d { | |||
| 428 | + | s.dnd = nil | |||
| 429 | + | } | |||
| 430 | + | if d.icon != nil { | |||
| 431 | + | d.icon.role = nil | |||
| 432 | + | d.icon = nil | |||
| 433 | + | } | |||
| 434 | + | if d.iconID != 0 { | |||
| 435 | + | s.emitContent(ports.SurfaceContent{ID: d.iconID}, damage{full: true}) | |||
| 436 | + | d.iconID = 0 | |||
| 437 | + | } | |||
| 438 | + | if ended { | |||
| 439 | + | s.log.Info().Msg("data drag ended") | |||
| 440 | + | s.emit(ports.DataDragEnded{}) | |||
| 441 | + | } | |||
| 442 | + | } | |||
| 443 | + | ||||
| 444 | + | // dndSourceGone ends the drags of a destroyed source. | |||
| 445 | + | func (s *Server) dndSourceGone(src *clipSource) { | |||
| 446 | + | if d := s.dnd; d != nil && d.src == src { | |||
| 447 | + | s.dndEnd(d, false, true) | |||
| 448 | + | } | |||
| 449 | + | for _, t := range slices.Clone(s.dndDrops) { | |||
| 450 | + | if t.d.src == src { | |||
| 451 | + | s.dndDropDone(t) | |||
| 452 | + | } | |||
| 453 | + | } | |||
| 454 | + | } | |||
| 455 | + | ||||
| 456 | + | // dndDropDone forgets a dropped target. | |||
| 457 | + | func (s *Server) dndDropDone(t *dndTarget) { | |||
| 458 | + | t.state = targetDone | |||
| 459 | + | s.dndDrops = slices.DeleteFunc(s.dndDrops, func(x *dndTarget) bool { return x == t }) | |||
| 460 | + | } | |||
| 461 | + | ||||
| 462 | + | // dndSurfaceGone clears the drag target when its surface is destroyed. | |||
| 463 | + | func (s *Server) dndSurfaceGone(surf *surface) { | |||
| 464 | + | if d := s.dnd; d != nil && d.target != nil && surf.wl != nil && d.target.surface.Resource == surf.wl.Resource { | |||
| 465 | + | s.dndLeave(d) | |||
| 466 | + | } | |||
| 467 | + | } | |||
| 468 | + | ||||
| 469 | + | // dndCancelAll cancels the drag and the drops waiting for their finish: the | |||
| 470 | + | // session is protected. | |||
| 471 | + | func (s *Server) dndCancelAll() { | |||
| 472 | + | if d := s.dnd; d != nil { | |||
| 473 | + | s.dndEnd(d, true, false) | |||
| 474 | + | } | |||
| 475 | + | for _, t := range slices.Clone(s.dndDrops) { | |||
| 476 | + | if src := t.d.src; src != nil && src.alive() { | |||
| 477 | + | src.cancel() | |||
| 478 | + | } | |||
| 479 | + | s.dndDropDone(t) | |||
| 480 | + | } | |||
| 481 | + | } | |||
| 482 | + | ||||
| 483 | + | // dndOfferGone runs when an offer of a drag is destroyed. With none left on | |||
| 484 | + | // a live target, nothing is accepted any more. With none left on a dropped | |||
| 485 | + | // one the transfer is over: a pre-v3 target has finished, any other | |||
| 486 | + | // cancelled it. | |||
| 487 | + | func (s *Server) dndOfferGone(o *dndOffer) { | |||
| 488 | + | t := o.t | |||
| 489 | + | t.offers = slices.DeleteFunc(t.offers, func(x *dndOffer) bool { return x == o }) | |||
| 490 | + | if len(t.offers) != 0 { | |||
| 491 | + | return | |||
| 492 | + | } | |||
| 493 | + | switch t.state { | |||
| 494 | + | case targetActive: | |||
| 495 | + | s.dndResetSource(t) | |||
| 496 | + | case targetDropped: | |||
| 497 | + | if src := t.d.src; src != nil && src.alive() { | |||
| 498 | + | if !t.legacy { | |||
| 499 | + | src.cancel() | |||
| 500 | + | } else if src.res.Version() >= 3 { | |||
| 501 | + | src.res.SendDndFinished() | |||
| 502 | + | } | |||
| 503 | + | } | |||
| 504 | + | s.dndDropDone(t) | |||
| 505 | + | } | |||
| 506 | + | } | |||
| 507 | + | ||||
| 508 | + | // update recomputes the action after the target or the source changed | |||
| 509 | + | // something, and tells both when it changed. After an ask drop the choice is | |||
| 510 | + | // silent until finish. | |||
| 511 | + | func (o *dndOffer) update() { | |||
| 512 | + | t, src := o.t, o.t.d.src | |||
| 513 | + | if t.state == targetDone || t.state == targetDropped && !t.ask || src == nil || !src.alive() { | |||
| 514 | + | return | |||
| 515 | + | } | |||
| 516 | + | action := chooseAction(o.actions&src.srcActions(), o.preferred) | |||
| 517 | + | if action != t.action { | |||
| 518 | + | t.action = action | |||
| 519 | + | if t.state == targetActive && src.res.Version() >= 3 { | |||
| 520 | + | src.res.SendAction(action) | |||
| 521 | + | } | |||
| 522 | + | } | |||
| 523 | + | if t.state == targetActive && action != o.sent { | |||
| 524 | + | o.sent = action | |||
| 525 | + | if o.res.Version() >= 3 { | |||
| 526 | + | o.res.SendAction(action) | |||
| 527 | + | } | |||
| 528 | + | } | |||
| 529 | + | } | |||
| 530 | + | ||||
| 531 | + | func (o *dndOffer) Accept(_ *wayland.DataOffer, _ uint32, mime string) { | |||
| 532 | + | t := o.t | |||
| 533 | + | if t.state != targetActive && !(t.state == targetDropped && t.ask) || !t.d.src.alive() { | |||
| 534 | + | return | |||
| 535 | + | } | |||
| 536 | + | t.accepted = mime | |||
| 537 | + | t.d.src.res.SendTarget(mime) | |||
| 538 | + | } | |||
| 539 | + | ||||
| 540 | + | func (o *dndOffer) Receive(_ *wayland.DataOffer, mime string, fd int) { | |||
| 541 | + | defer unix.Close(fd) | |||
| 542 | + | src := o.t.d.src | |||
| 543 | + | if o.server.protected() || o.t.state == targetDone || !src.alive() { | |||
| 544 | + | return | |||
| 545 | + | } | |||
| 546 | + | if slices.Contains(src.mimes, mime) { | |||
| 547 | + | src.send(mime, fd) | |||
| 548 | + | } | |||
| 549 | + | } | |||
| 550 | + | ||||
| 551 | + | func (*dndOffer) Destroy(*wayland.DataOffer) {} | |||
| 552 | + | ||||
| 553 | + | func (o *dndOffer) Finish(r *wayland.DataOffer) { | |||
| 554 | + | t := o.t | |||
| 555 | + | switch t.state { | |||
| 556 | + | case targetDone: | |||
| 557 | + | return | |||
| 558 | + | case targetActive: | |||
| 559 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish before the drop") | |||
| 560 | + | return | |||
| 561 | + | } | |||
| 562 | + | if t.action == 0 || t.action == actionAsk { | |||
| 563 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without a final action") | |||
| 564 | + | return | |||
| 565 | + | } | |||
| 566 | + | if t.accepted == "" { | |||
| 567 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidFinish), "finish without an accepted MIME type") | |||
| 568 | + | return | |||
| 569 | + | } | |||
| 570 | + | o.server.dndDropDone(t) | |||
| 571 | + | if src := t.d.src; src.alive() && src.res.Version() >= 3 { | |||
| 572 | + | if t.ask { | |||
| 573 | + | src.res.SendAction(t.action) | |||
| 574 | + | } | |||
| 575 | + | src.res.SendDndFinished() | |||
| 576 | + | } | |||
| 577 | + | } | |||
| 578 | + | ||||
| 579 | + | func (o *dndOffer) SetActions(r *wayland.DataOffer, actions, preferred uint32) { | |||
| 580 | + | if actions&^dndAllActions != 0 { | |||
| 581 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidActionMask), "invalid action mask") | |||
| 582 | + | return | |||
| 583 | + | } | |||
| 584 | + | if preferred != 0 && (preferred&(preferred-1) != 0 || preferred&actions == 0) { | |||
| 585 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "invalid preferred action") | |||
| 586 | + | return | |||
| 587 | + | } | |||
| 588 | + | if t := o.t; t.state == targetDropped && t.ask && preferred != 0 && t.d.src != nil && preferred&t.d.src.srcActions() == 0 { | |||
| 589 | + | r.PostError(uint32(wayland.DataOfferErrorInvalidAction), "final action not allowed by the source") | |||
| 590 | + | return | |||
| 591 | + | } | |||
| 592 | + | o.actions, o.preferred = actions, preferred | |||
| 593 | + | o.update() | |||
| 594 | + | } | |||
A internal/adapters/wayland/dnd_test.go +1154 -0
| @@ -0,0 +1,1154 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 6 | + | "sync/atomic" | |
| 7 | + | "testing" | |
| 8 | + | "time" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 11 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |
| 12 | + | "github.com/bnema/neferwl/internal/adapters/sessionsecurity" | |
| 13 | + | "github.com/bnema/neferwl/internal/ports" | |
| 14 | + | "github.com/bnema/wlturbo" | |
| 15 | + | "golang.org/x/sys/unix" | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | const ( | |
| 19 | + | actCopy = uint32(wayland.DataDeviceManagerDndActionCopy) | |
| 20 | + | actMove = uint32(wayland.DataDeviceManagerDndActionMove) | |
| 21 | + | actAsk = uint32(wayland.DataDeviceManagerDndActionAsk) | |
| 22 | + | ) | |
| 23 | + | ||
| 24 | + | // dndEvent is one data device, offer or source event a test client saw. | |
| 25 | + | type dndEvent struct { | |
| 26 | + | name string | |
| 27 | + | mime string | |
| 28 | + | x, y float64 | |
| 29 | + | n uint32 | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | type dndDeviceEvents struct { | |
| 33 | + | wlturbo.BaseProxy | |
| 34 | + | c *wlturbo.Display | |
| 35 | + | log chan dndEvent | |
| 36 | + | // offer is the id of the latest data_offer. | |
| 37 | + | offer atomic.Uint32 | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | func (p *dndDeviceEvents) Dispatch(e *wlturbo.Event) { | |
| 41 | + | switch e.Opcode { | |
| 42 | + | case uint16(wayland.DataDeviceEventDataOffer): | |
| 43 | + | o := &dndOfferEvents{log: p.log} | |
| 44 | + | o.SetID(e.Uint32()) | |
| 45 | + | p.offer.Store(o.ID()) | |
| 46 | + | registerWireProxy(p.c, o) | |
| 47 | + | case uint16(wayland.DataDeviceEventEnter): | |
| 48 | + | serial, _ := e.Uint32(), e.Uint32() | |
| 49 | + | x, y := e.Fixed().Float64(), e.Fixed().Float64() | |
| 50 | + | p.log <- dndEvent{name: "enter", x: x, y: y, n: e.Uint32() + serial*0} | |
| 51 | + | case uint16(wayland.DataDeviceEventLeave): | |
| 52 | + | p.log <- dndEvent{name: "leave"} | |
| 53 | + | case uint16(wayland.DataDeviceEventMotion): | |
| 54 | + | _ = e.Uint32() | |
| 55 | + | p.log <- dndEvent{name: "motion", x: e.Fixed().Float64(), y: e.Fixed().Float64()} | |
| 56 | + | case uint16(wayland.DataDeviceEventDrop): | |
| 57 | + | p.log <- dndEvent{name: "drop"} | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | type dndOfferEvents struct { | |
| 62 | + | wlturbo.BaseProxy | |
| 63 | + | log chan dndEvent | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | func (p *dndOfferEvents) Dispatch(e *wlturbo.Event) { | |
| 67 | + | switch e.Opcode { | |
| 68 | + | case uint16(wayland.DataOfferEventOffer): | |
| 69 | + | p.log <- dndEvent{name: "offer", mime: e.String()} | |
| 70 | + | case uint16(wayland.DataOfferEventSourceActions): | |
| 71 | + | p.log <- dndEvent{name: "source_actions", n: e.Uint32()} | |
| 72 | + | case uint16(wayland.DataOfferEventAction): | |
| 73 | + | p.log <- dndEvent{name: "offer_action", n: e.Uint32()} | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | type dndSourceEvents struct { | |
| 78 | + | wlturbo.BaseProxy | |
| 79 | + | log chan dndEvent | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | func (p *dndSourceEvents) Dispatch(e *wlturbo.Event) { | |
| 83 | + | switch e.Opcode { | |
| 84 | + | case uint16(wayland.DataSourceEventTarget): | |
| 85 | + | p.log <- dndEvent{name: "target", mime: e.String()} | |
| 86 | + | case uint16(wayland.DataSourceEventSend): | |
| 87 | + | mime := e.String() | |
| 88 | + | fd := int(e.Fd()) | |
| 89 | + | _, _ = unix.Write(fd, []byte("dnd data")) | |
| 90 | + | _ = unix.Close(fd) | |
| 91 | + | p.log <- dndEvent{name: "send", mime: mime} | |
| 92 | + | case uint16(wayland.DataSourceEventCancelled): | |
| 93 | + | p.log <- dndEvent{name: "cancelled"} | |
| 94 | + | case uint16(wayland.DataSourceEventDndDropPerformed): | |
| 95 | + | p.log <- dndEvent{name: "drop_performed"} | |
| 96 | + | case uint16(wayland.DataSourceEventDndFinished): | |
| 97 | + | p.log <- dndEvent{name: "finished"} | |
| 98 | + | case uint16(wayland.DataSourceEventAction): | |
| 99 | + | p.log <- dndEvent{name: "action", n: e.Uint32()} | |
| 100 | + | } | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | // dndClient is a client with a toplevel, a pointer and a data device. | |
| 104 | + | type dndClient struct { | |
| 105 | + | keys chan uint32 | |
| 106 | + | c *wlturbo.Display | |
| 107 | + | manager uint32 | |
| 108 | + | device *dndDeviceEvents | |
| 109 | + | serials chan uint32 | |
| 110 | + | comp uint32 | |
| 111 | + | shm uint32 | |
| 112 | + | surf uint32 | |
| 113 | + | window ports.WindowID | |
| 114 | + | log chan dndEvent | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | // keySerials records the serial of each key event. | |
| 118 | + | type keySerials struct { | |
| 119 | + | wlturbo.BaseProxy | |
| 120 | + | serials chan uint32 | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | func (p *keySerials) Dispatch(e *wlturbo.Event) { | |
| 124 | + | if e.Opcode == uint16(wayland.KeyboardEventKey) { | |
| 125 | + | p.serials <- e.Uint32() | |
| 126 | + | } | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | func newDndClient(t *testing.T, s *Server, dir string, events <-chan ports.ClientEvent) *dndClient { | |
| 130 | + | t.Helper() | |
| 131 | + | return newDndClientAt(t, s, dir, events, 3) | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | // newDndClientAt is newDndClient with wl_data_device_manager at version. | |
| 135 | + | func newDndClientAt(t *testing.T, s *Server, dir string, events <-chan ports.ClientEvent, version uint32) *dndClient { | |
| 136 | + | t.Helper() | |
| 137 | + | c := protocolClient(t, s, dir) | |
| 138 | + | d := &dndClient{c: c, log: make(chan dndEvent, 64), serials: make(chan uint32, 8), keys: make(chan uint32, 8)} | |
| 139 | + | kb := &keySerials{serials: d.keys} | |
| 140 | + | seat := bindVersion(t, c, "wl_seat", 8) | |
| 141 | + | registerProtocol(t, c, seat) | |
| 142 | + | ptr := &pressSerials{serials: d.serials} | |
| 143 | + | ptr.SetID(c.AllocateID()) | |
| 144 | + | registerWireProxy(c, ptr) | |
| 145 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, ptr.ID()) | |
| 146 | + | kb.SetID(c.AllocateID()) | |
| 147 | + | registerWireProxy(c, kb) | |
| 148 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, kb.ID()) | |
| 149 | + | d.manager = bindVersion(t, c, "wl_data_device_manager", version) | |
| 150 | + | d.device = &dndDeviceEvents{c: c, log: d.log} | |
| 151 | + | d.device.SetID(c.AllocateID()) | |
| 152 | + | registerWireProxy(c, d.device) | |
| 153 | + | requestProtocol(t, c, d.manager, wayland.DataDeviceManagerRequestGetDataDevice, d.device.ID(), seat) | |
| 154 | + | d.comp = bindVersion(t, c, "wl_compositor", 5) | |
| 155 | + | d.shm = bindProtocol(t, c, "wl_shm") | |
| 156 | + | registerProtocol(t, c, d.shm) | |
| 157 | + | w, surf, _ := surfaceMapperAt(t, c, events, 5)() | |
| 158 | + | d.window, d.surf = w.ID, surf | |
| 159 | + | return d | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | // lastOffer is the id of the latest offer the client got. | |
| 163 | + | func (d *dndClient) lastOffer(t *testing.T) uint32 { | |
| 164 | + | t.Helper() | |
| 165 | + | id := d.device.offer.Load() | |
| 166 | + | if id == 0 { | |
| 167 | + | t.Fatal("no offer") | |
| 168 | + | } | |
| 169 | + | return id | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | // next returns the next event the client sees. | |
| 173 | + | func (d *dndClient) next(t *testing.T) dndEvent { | |
| 174 | + | t.Helper() | |
| 175 | + | deadline := time.After(2 * time.Second) | |
| 176 | + | for { | |
| 177 | + | if err := d.c.Roundtrip(); err != nil { | |
| 178 | + | t.Fatal(err) | |
| 179 | + | } | |
| 180 | + | select { | |
| 181 | + | case ev := <-d.log: | |
| 182 | + | return ev | |
| 183 | + | case <-deadline: | |
| 184 | + | t.Fatal("no event") | |
| 185 | + | case <-time.After(5 * time.Millisecond): | |
| 186 | + | } | |
| 187 | + | } | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // expect waits for the next event and checks its name. | |
| 191 | + | func (d *dndClient) expect(t *testing.T, name string) dndEvent { | |
| 192 | + | t.Helper() | |
| 193 | + | ev := d.next(t) | |
| 194 | + | if ev.name != name { | |
| 195 | + | t.Fatalf("event %+v, want %s", ev, name) | |
| 196 | + | } | |
| 197 | + | return ev | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | // none checks that no event arrives. | |
| 201 | + | func (d *dndClient) none(t *testing.T) { | |
| 202 | + | t.Helper() | |
| 203 | + | for range 2 { | |
| 204 | + | if err := d.c.Roundtrip(); err != nil { | |
| 205 | + | t.Fatal(err) | |
| 206 | + | } | |
| 207 | + | time.Sleep(30 * time.Millisecond) | |
| 208 | + | } | |
| 209 | + | select { | |
| 210 | + | case ev := <-d.log: | |
| 211 | + | t.Fatalf("unexpected event %+v", ev) | |
| 212 | + | default: | |
| 213 | + | } | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | func (d *dndClient) source(t *testing.T, actions uint32, mimes ...string) uint32 { | |
| 217 | + | t.Helper() | |
| 218 | + | src := &dndSourceEvents{log: d.log} | |
| 219 | + | src.SetID(d.c.AllocateID()) | |
| 220 | + | registerWireProxy(d.c, src) | |
| 221 | + | requestProtocol(t, d.c, d.manager, wayland.DataDeviceManagerRequestCreateDataSource, src.ID()) | |
| 222 | + | for _, m := range mimes { | |
| 223 | + | requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestOffer, m) | |
| 224 | + | } | |
| 225 | + | if actions != 0 { | |
| 226 | + | requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestSetActions, actions) | |
| 227 | + | } | |
| 228 | + | return src.ID() | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | // press makes the client's window press a button and returns the serial. | |
| 232 | + | func (d *dndClient) press(t *testing.T, commands chan<- ports.ClientCommand) uint32 { | |
| 233 | + | t.Helper() | |
| 234 | + | commands <- ports.PointerFocus{ID: d.window, X: 1, Y: 1} | |
| 235 | + | commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Pressed: true, Grab: true, Time: time.Millisecond} | |
| 236 | + | deadline := time.After(2 * time.Second) | |
| 237 | + | for { | |
| 238 | + | if err := d.c.Roundtrip(); err != nil { | |
| 239 | + | t.Fatal(err) | |
| 240 | + | } | |
| 241 | + | select { | |
| 242 | + | case serial := <-d.serials: | |
| 243 | + | return serial | |
| 244 | + | case <-deadline: | |
| 245 | + | t.Fatal("no button") | |
| 246 | + | case <-time.After(5 * time.Millisecond): | |
| 247 | + | } | |
| 248 | + | } | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | // press2 returns the next pointer button serial. | |
| 252 | + | func (d *dndClient) press2(t *testing.T) uint32 { | |
| 253 | + | t.Helper() | |
| 254 | + | deadline := time.After(2 * time.Second) | |
| 255 | + | for { | |
| 256 | + | if err := d.c.Roundtrip(); err != nil { | |
| 257 | + | t.Fatal(err) | |
| 258 | + | } | |
| 259 | + | select { | |
| 260 | + | case serial := <-d.serials: | |
| 261 | + | return serial | |
| 262 | + | case <-deadline: | |
| 263 | + | t.Fatal("no button") | |
| 264 | + | case <-time.After(5 * time.Millisecond): | |
| 265 | + | } | |
| 266 | + | } | |
| 267 | + | } | |
| 268 | + | ||
| 269 | + | // release releases the button press took and waits for the client to see it. | |
| 270 | + | func (d *dndClient) release(t *testing.T, commands chan<- ports.ClientCommand) { | |
| 271 | + | t.Helper() | |
| 272 | + | commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Time: 2 * time.Millisecond} | |
| 273 | + | d.press2(t) | |
| 274 | + | } | |
| 275 | + | ||
| 276 | + | // key presses a key on the client's focused window and returns the serial. | |
| 277 | + | func (d *dndClient) key(t *testing.T, commands chan<- ports.ClientCommand) uint32 { | |
| 278 | + | t.Helper() | |
| 279 | + | commands <- ports.FocusWindow{ID: d.window} | |
| 280 | + | commands <- ports.ForwardKey{ID: d.window, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 3 * time.Millisecond}} | |
| 281 | + | deadline := time.After(2 * time.Second) | |
| 282 | + | for { | |
| 283 | + | if err := d.c.Roundtrip(); err != nil { | |
| 284 | + | t.Fatal(err) | |
| 285 | + | } | |
| 286 | + | select { | |
| 287 | + | case serial := <-d.keys: | |
| 288 | + | return serial | |
| 289 | + | case <-deadline: | |
| 290 | + | t.Fatal("no key") | |
| 291 | + | case <-time.After(5 * time.Millisecond): | |
| 292 | + | } | |
| 293 | + | } | |
| 294 | + | } | |
| 295 | + | ||
| 296 | + | // events returns what the client saw up to and including name. | |
| 297 | + | func (d *dndClient) until(t *testing.T, name string) []dndEvent { | |
| 298 | + | t.Helper() | |
| 299 | + | var got []dndEvent | |
| 300 | + | for { | |
| 301 | + | ev := d.next(t) | |
| 302 | + | got = append(got, ev) | |
| 303 | + | if ev.name == name { | |
| 304 | + | return got | |
| 305 | + | } | |
| 306 | + | } | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | func (d *dndClient) startDrag(t *testing.T, src, icon, serial uint32) { | |
| 310 | + | t.Helper() | |
| 311 | + | d.startDragFrom(t, src, d.surf, icon, serial) | |
| 312 | + | } | |
| 313 | + | ||
| 314 | + | // startDragFrom is startDrag with an origin surface. | |
| 315 | + | func (d *dndClient) startDragFrom(t *testing.T, src, origin, icon, serial uint32) { | |
| 316 | + | t.Helper() | |
| 317 | + | requestProtocol(t, d.c, d.device.ID(), wayland.DataDeviceRequestStartDrag, src, origin, icon, serial) | |
| 318 | + | if err := d.c.Roundtrip(); err != nil { | |
| 319 | + | t.Fatal(err) | |
| 320 | + | } | |
| 321 | + | } | |
| 322 | + | ||
| 323 | + | // buffer makes a w×h ARGB shm buffer. | |
| 324 | + | func (d *dndClient) buffer(t *testing.T, w, h int) uint32 { | |
| 325 | + | t.Helper() | |
| 326 | + | fd, err := unix.MemfdCreate("dnd-icon", 0) | |
| 327 | + | if err != nil { | |
| 328 | + | t.Fatal(err) | |
| 329 | + | } | |
| 330 | + | defer unix.Close(fd) | |
| 331 | + | if err := unix.Ftruncate(fd, int64(w*h*4)); err != nil { | |
| 332 | + | t.Fatal(err) | |
| 333 | + | } | |
| 334 | + | pool, buf := d.c.AllocateID(), d.c.AllocateID() | |
| 335 | + | registerProtocol(t, d.c, pool) | |
| 336 | + | registerProtocol(t, d.c, buf) | |
| 337 | + | if err := wireRequest(d.c, d.shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(w*h*4)); err != nil { | |
| 338 | + | t.Fatal(err) | |
| 339 | + | } | |
| 340 | + | requestProtocol(t, d.c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(w), int32(h), int32(w*4), uint32(0)) | |
| 341 | + | return buf | |
| 342 | + | } | |
| 343 | + | ||
| 344 | + | func (d *dndClient) surfaceNew(t *testing.T) uint32 { | |
| 345 | + | t.Helper() | |
| 346 | + | id := d.c.AllocateID() | |
| 347 | + | requestProtocol(t, d.c, d.comp, wayland.CompositorRequestCreateSurface, id) | |
| 348 | + | registerProtocol(t, d.c, id) | |
| 349 | + | return id | |
| 350 | + | } | |
| 351 | + | ||
| 352 | + | // dndNextEvent returns the next client event of type T, skipping others. | |
| 353 | + | func dndNextEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T { | |
| 354 | + | t.Helper() | |
| 355 | + | deadline := time.After(2 * time.Second) | |
| 356 | + | for { | |
| 357 | + | select { | |
| 358 | + | case ev := <-events: | |
| 359 | + | if v, ok := ev.(T); ok { | |
| 360 | + | return v | |
| 361 | + | } | |
| 362 | + | case <-deadline: | |
| 363 | + | t.Fatalf("no %T event", *new(T)) | |
| 364 | + | } | |
| 365 | + | } | |
| 366 | + | } | |
| 367 | + | ||
| 368 | + | // dndNoEvent checks that no client event of type T is pending. | |
| 369 | + | func dndNoEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) { | |
| 370 | + | t.Helper() | |
| 371 | + | time.Sleep(50 * time.Millisecond) | |
| 372 | + | for { | |
| 373 | + | select { | |
| 374 | + | case ev := <-events: | |
| 375 | + | if _, ok := ev.(T); ok { | |
| 376 | + | t.Fatalf("unexpected %T: %+v", ev, ev) | |
| 377 | + | } | |
| 378 | + | default: | |
| 379 | + | return | |
| 380 | + | } | |
| 381 | + | } | |
| 382 | + | } | |
| 383 | + | ||
| 384 | + | // dndPair is two clients with a drag started from a on a source. | |
| 385 | + | type dndPair struct { | |
| 386 | + | s *Server | |
| 387 | + | events chan ports.ClientEvent | |
| 388 | + | commands chan ports.ClientCommand | |
| 389 | + | a, b *dndClient | |
| 390 | + | } | |
| 391 | + | ||
| 392 | + | func newDndPair(t *testing.T) *dndPair { | |
| 393 | + | t.Helper() | |
| 394 | + | s, events, commands, dir := keyboardServer(t) | |
| 395 | + | p := &dndPair{s: s, events: events, commands: commands} | |
| 396 | + | p.a = newDndClient(t, s, dir, events) | |
| 397 | + | p.b = newDndClient(t, s, dir, events) | |
| 398 | + | return p | |
| 399 | + | } | |
| 400 | + | ||
| 401 | + | // start begins a drag of a source offering text/plain, copy and move. | |
| 402 | + | func (p *dndPair) start(t *testing.T, icon uint32) uint32 { | |
| 403 | + | t.Helper() | |
| 404 | + | src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list") | |
| 405 | + | p.a.startDrag(t, src, icon, p.a.press(t, p.commands)) | |
| 406 | + | return src | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | // enter makes b the drag target and returns the offer id. | |
| 410 | + | func (p *dndPair) enter(t *testing.T) uint32 { | |
| 411 | + | t.Helper() | |
| 412 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6, Time: 10 * time.Millisecond} | |
| 413 | + | for _, want := range []dndEvent{{name: "offer", mime: "text/plain"}, {name: "offer", mime: "text/uri-list"}, {name: "source_actions", n: actCopy | actMove}} { | |
| 414 | + | if ev := p.b.next(t); ev != want { | |
| 415 | + | t.Fatalf("event %+v, want %+v", ev, want) | |
| 416 | + | } | |
| 417 | + | } | |
| 418 | + | ev := p.b.expect(t, "enter") | |
| 419 | + | if ev.x != 5 || ev.y != 6 || ev.n == 0 { | |
| 420 | + | t.Fatalf("enter %+v", ev) | |
| 421 | + | } | |
| 422 | + | return ev.n | |
| 423 | + | } | |
| 424 | + | ||
| 425 | + | // A1: a drag from a to b delivers enter, motion and drop with the offer; | |
| 426 | + | // the action is negotiated; the data reaches b; a is told every step. | |
| 427 | + | func TestDataDragBetweenClients(t *testing.T) { | |
| 428 | + | p := newDndPair(t) | |
| 429 | + | p.start(t, 0) | |
| 430 | + | if v := dndNextEvent[ports.DataDragStarted](t, p.events); v.Icon != 0 { | |
| 431 | + | t.Fatalf("started %+v", v) | |
| 432 | + | } | |
| 433 | + | offer := p.enter(t) | |
| 434 | + | ||
| 435 | + | p.commands <- ports.DataDragMotion{ID: p.b.window, X: 7, Y: 8, Time: 20 * time.Millisecond} | |
| 436 | + | if ev := p.b.expect(t, "motion"); ev.x != 7 || ev.y != 8 { | |
| 437 | + | t.Fatalf("motion %+v", ev) | |
| 438 | + | } | |
| 439 | + | ||
| 440 | + | // b accepts and prefers move: both sides learn the action. | |
| 441 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |
| 442 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove) | |
| 443 | + | if ev := p.a.expect(t, "target"); ev.mime != "text/plain" { | |
| 444 | + | t.Fatalf("target %+v", ev) | |
| 445 | + | } | |
| 446 | + | if ev := p.a.expect(t, "action"); ev.n != actMove { | |
| 447 | + | t.Fatalf("action %+v", ev) | |
| 448 | + | } | |
| 449 | + | if ev := p.b.expect(t, "offer_action"); ev.n != actMove { | |
| 450 | + | t.Fatalf("offer action %+v", ev) | |
| 451 | + | } | |
| 452 | + | ||
| 453 | + | // Finishing before the drop is a protocol error, so only after it. | |
| 454 | + | p.commands <- ports.DataDragDrop{} | |
| 455 | + | p.b.expect(t, "drop") | |
| 456 | + | p.b.expect(t, "leave") | |
| 457 | + | p.a.expect(t, "drop_performed") | |
| 458 | + | ||
| 459 | + | var fds [2]int | |
| 460 | + | if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil { | |
| 461 | + | t.Fatal(err) | |
| 462 | + | } | |
| 463 | + | defer unix.Close(fds[0]) | |
| 464 | + | if err := wireRequest(p.b.c, offer, uint16(wayland.DataOfferRequestReceive), []int{fds[1]}, "text/plain"); err != nil { | |
| 465 | + | t.Fatal(err) | |
| 466 | + | } | |
| 467 | + | _ = unix.Close(fds[1]) | |
| 468 | + | if err := p.b.c.Roundtrip(); err != nil { | |
| 469 | + | t.Fatal(err) | |
| 470 | + | } | |
| 471 | + | if ev := p.a.expect(t, "send"); ev.mime != "text/plain" { | |
| 472 | + | t.Fatalf("send %+v", ev) | |
| 473 | + | } | |
| 474 | + | buf := make([]byte, 16) | |
| 475 | + | n, err := unix.Read(fds[0], buf) | |
| 476 | + | for errors.Is(err, unix.EINTR) { | |
| 477 | + | n, err = unix.Read(fds[0], buf) | |
| 478 | + | } | |
| 479 | + | if err != nil || string(buf[:n]) != "dnd data" { | |
| 480 | + | t.Fatalf("received %q, %v", buf[:max(n, 0)], err) | |
| 481 | + | } | |
| 482 | + | ||
| 483 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish) | |
| 484 | + | p.a.expect(t, "finished") | |
| 485 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy) | |
| 486 | + | p.a.none(t) | |
| 487 | + | p.b.none(t) | |
| 488 | + | // core ended the drag: wayland does not echo it. | |
| 489 | + | dndNoEvent[ports.DataDragEnded](t, p.events) | |
| 490 | + | } | |
| 491 | + | ||
| 492 | + | // The action follows wlroots: the preferred one if both allow it, else the | |
| 493 | + | // first of copy, move, ask they share, else none. | |
| 494 | + | func TestChooseAction(t *testing.T) { | |
| 495 | + | for _, tc := range []struct{ avail, preferred, want uint32 }{ | |
| 496 | + | {actCopy | actMove, actMove, actMove}, | |
| 497 | + | {actCopy | actMove, actAsk, actCopy}, | |
| 498 | + | {actMove | actAsk, 0, actMove}, | |
| 499 | + | {actAsk, 0, actAsk}, | |
| 500 | + | {0, actCopy, 0}, | |
| 501 | + | } { | |
| 502 | + | if got := chooseAction(tc.avail, tc.preferred); got != tc.want { | |
| 503 | + | t.Errorf("chooseAction(%d, %d) = %d, want %d", tc.avail, tc.preferred, got, tc.want) | |
| 504 | + | } | |
| 505 | + | } | |
| 506 | + | } | |
| 507 | + | ||
| 508 | + | // A target and a source sharing no action leave the drop refused. | |
| 509 | + | func TestDataDragNoCommonAction(t *testing.T) { | |
| 510 | + | p := newDndPair(t) | |
| 511 | + | src := p.a.source(t, actCopy, "text/plain") | |
| 512 | + | p.a.startDrag(t, src, 0, p.a.press(t, p.commands)) | |
| 513 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |
| 514 | + | p.b.expect(t, "offer") | |
| 515 | + | p.b.expect(t, "source_actions") | |
| 516 | + | offer := p.b.expect(t, "enter").n | |
| 517 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |
| 518 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actMove, actMove) | |
| 519 | + | p.a.expect(t, "target") | |
| 520 | + | p.commands <- ports.DataDragDrop{} | |
| 521 | + | p.b.expect(t, "leave") | |
| 522 | + | // The refused target no longer accepts. | |
| 523 | + | if ev := p.a.expect(t, "target"); ev.mime != "" { | |
| 524 | + | t.Fatalf("target %+v", ev) | |
| 525 | + | } | |
| 526 | + | p.a.expect(t, "cancelled") | |
| 527 | + | } | |
| 528 | + | ||
| 529 | + | // A2: a release over nothing, or over a target that accepted nothing, | |
| 530 | + | // cancels the source; the target gets leave and no drop. | |
| 531 | + | func TestDataDragRefusedDrop(t *testing.T) { | |
| 532 | + | t.Run("nothing", func(t *testing.T) { | |
| 533 | + | p := newDndPair(t) | |
| 534 | + | p.start(t, 0) | |
| 535 | + | p.commands <- ports.DataDragDrop{} | |
| 536 | + | p.a.expect(t, "cancelled") | |
| 537 | + | p.b.none(t) | |
| 538 | + | }) | |
| 539 | + | t.Run("not accepted", func(t *testing.T) { | |
| 540 | + | p := newDndPair(t) | |
| 541 | + | p.start(t, 0) | |
| 542 | + | p.enter(t) | |
| 543 | + | p.commands <- ports.DataDragDrop{} | |
| 544 | + | p.b.expect(t, "leave") | |
| 545 | + | p.a.expect(t, "cancelled") | |
| 546 | + | p.b.none(t) | |
| 547 | + | }) | |
| 548 | + | t.Run("escape", func(t *testing.T) { | |
| 549 | + | p := newDndPair(t) | |
| 550 | + | p.start(t, 0) | |
| 551 | + | p.enter(t) | |
| 552 | + | p.commands <- ports.DataDragCancel{} | |
| 553 | + | p.b.expect(t, "leave") | |
| 554 | + | p.a.expect(t, "cancelled") | |
| 555 | + | dndNoEvent[ports.DataDragEnded](t, p.events) | |
| 556 | + | }) | |
| 557 | + | } | |
| 558 | + | ||
| 559 | + | // Moving to another surface leaves the first and enters the second; a | |
| 560 | + | // surface of nobody (ID 0) leaves. | |
| 561 | + | func TestDataDragLeavesOnFocusChange(t *testing.T) { | |
| 562 | + | p := newDndPair(t) | |
| 563 | + | p.start(t, 0) | |
| 564 | + | p.enter(t) | |
| 565 | + | p.commands <- ports.DataDragFocus{} | |
| 566 | + | p.b.expect(t, "leave") | |
| 567 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 2, Y: 3} | |
| 568 | + | for range 3 { | |
| 569 | + | p.b.next(t) | |
| 570 | + | } | |
| 571 | + | if ev := p.b.expect(t, "enter"); ev.x != 2 || ev.y != 3 { | |
| 572 | + | t.Fatalf("enter %+v", ev) | |
| 573 | + | } | |
| 574 | + | } | |
| 575 | + | ||
| 576 | + | // A4: the source dying mid-drag ends it: the target gets leave and core a | |
| 577 | + | // DataDragEnded; later commands change nothing. | |
| 578 | + | func TestDataDragSourceDestroyed(t *testing.T) { | |
| 579 | + | p := newDndPair(t) | |
| 580 | + | src := p.start(t, 0) | |
| 581 | + | p.enter(t) | |
| 582 | + | requestProtocol(t, p.a.c, src, wayland.DataSourceRequestDestroy) | |
| 583 | + | if err := p.a.c.Roundtrip(); err != nil { | |
| 584 | + | t.Fatal(err) | |
| 585 | + | } | |
| 586 | + | p.b.expect(t, "leave") | |
| 587 | + | dndNextEvent[ports.DataDragEnded](t, p.events) | |
| 588 | + | p.commands <- ports.DataDragMotion{ID: p.b.window, X: 9, Y: 9} | |
| 589 | + | p.commands <- ports.DataDragDrop{} | |
| 590 | + | p.b.none(t) | |
| 591 | + | } | |
| 592 | + | ||
| 593 | + | // A drag whose origin client dies ends the same way. | |
| 594 | + | func TestDataDragOriginDies(t *testing.T) { | |
| 595 | + | p := newDndPair(t) | |
| 596 | + | p.start(t, 0) | |
| 597 | + | p.enter(t) | |
| 598 | + | _ = p.a.c.Close() | |
| 599 | + | p.b.expect(t, "leave") | |
| 600 | + | dndNextEvent[ports.DataDragEnded](t, p.events) | |
| 601 | + | } | |
| 602 | + | ||
| 603 | + | // A6: a start_drag without the current press of the origin client is | |
| 604 | + | // refused: the source is cancelled and core hears nothing. | |
| 605 | + | func TestDataDragInvalidStart(t *testing.T) { | |
| 606 | + | t.Run("stale serial", func(t *testing.T) { | |
| 607 | + | p := newDndPair(t) | |
| 608 | + | serial := p.a.press(t, p.commands) | |
| 609 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial-1) | |
| 610 | + | p.a.expect(t, "cancelled") | |
| 611 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 612 | + | }) | |
| 613 | + | t.Run("other client's press", func(t *testing.T) { | |
| 614 | + | p := newDndPair(t) | |
| 615 | + | p.a.press(t, p.commands) | |
| 616 | + | serial := p.b.press(t, p.commands) | |
| 617 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |
| 618 | + | p.a.expect(t, "cancelled") | |
| 619 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 620 | + | }) | |
| 621 | + | t.Run("key serial", func(t *testing.T) { | |
| 622 | + | p := newDndPair(t) | |
| 623 | + | p.a.press(t, p.commands) | |
| 624 | + | p.a.release(t, p.commands) | |
| 625 | + | serial := p.a.key(t, p.commands) | |
| 626 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |
| 627 | + | p.a.expect(t, "cancelled") | |
| 628 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 629 | + | }) | |
| 630 | + | t.Run("serial after the release", func(t *testing.T) { | |
| 631 | + | p := newDndPair(t) | |
| 632 | + | serial := p.a.press(t, p.commands) | |
| 633 | + | p.a.release(t, p.commands) | |
| 634 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |
| 635 | + | p.a.expect(t, "cancelled") | |
| 636 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 637 | + | }) | |
| 638 | + | t.Run("other surface of the client", func(t *testing.T) { | |
| 639 | + | p := newDndPair(t) | |
| 640 | + | serial := p.a.press(t, p.commands) | |
| 641 | + | other := p.a.surfaceNew(t) | |
| 642 | + | p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), other, 0, serial) | |
| 643 | + | p.a.expect(t, "cancelled") | |
| 644 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 645 | + | }) | |
| 646 | + | t.Run("press of the first button", func(t *testing.T) { | |
| 647 | + | // A second button does not make a new grab: only the first serial. | |
| 648 | + | p := newDndPair(t) | |
| 649 | + | first := p.a.press(t, p.commands) | |
| 650 | + | p.commands <- ports.PointerButtonTo{ID: p.a.window, Button: 0x111, Pressed: true, Time: 2 * time.Millisecond} | |
| 651 | + | second := p.a.press2(t) | |
| 652 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, second) | |
| 653 | + | p.a.expect(t, "cancelled") | |
| 654 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, first) | |
| 655 | + | // Core is told the grab's button: its release drops. | |
| 656 | + | if ev := dndNextEvent[ports.DataDragStarted](t, p.events); ev.Button != 0x110 { | |
| 657 | + | t.Fatalf("drag button %#x, want the grab's", ev.Button) | |
| 658 | + | } | |
| 659 | + | }) | |
| 660 | + | t.Run("grab release consumed by core", func(t *testing.T) { | |
| 661 | + | // A window drag consumes the release: the next grab press | |
| 662 | + | // still makes the serial a drag can name. | |
| 663 | + | p := newDndPair(t) | |
| 664 | + | stale := p.a.press(t, p.commands) | |
| 665 | + | serial := p.a.press(t, p.commands) | |
| 666 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, stale) | |
| 667 | + | p.a.expect(t, "cancelled") | |
| 668 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |
| 669 | + | dndNextEvent[ports.DataDragStarted](t, p.events) | |
| 670 | + | }) | |
| 671 | + | t.Run("grab release of a gone window", func(t *testing.T) { | |
| 672 | + | p := newDndPair(t) | |
| 673 | + | serial := p.a.press(t, p.commands) | |
| 674 | + | p.commands <- ports.PointerButtonTo{ID: 9999, Button: 0x110, Time: 2 * time.Millisecond} | |
| 675 | + | p.a.key(t, p.commands) // the release is applied once the key is seen | |
| 676 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |
| 677 | + | p.a.expect(t, "cancelled") | |
| 678 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 679 | + | }) | |
| 680 | + | t.Run("used source", func(t *testing.T) { | |
| 681 | + | p := newDndPair(t) | |
| 682 | + | src := p.start(t, 0) | |
| 683 | + | p.commands <- ports.DataDragCancel{} | |
| 684 | + | p.a.expect(t, "cancelled") | |
| 685 | + | p.a.startDrag(t, src, 0, p.a.press(t, p.commands)) | |
| 686 | + | p.a.expect(t, "cancelled") | |
| 687 | + | }) | |
| 688 | + | t.Run("drag in progress", func(t *testing.T) { | |
| 689 | + | p := newDndPair(t) | |
| 690 | + | p.start(t, 0) | |
| 691 | + | dndNextEvent[ports.DataDragStarted](t, p.events) | |
| 692 | + | other := p.a.source(t, actCopy, "text/plain") | |
| 693 | + | p.a.startDrag(t, other, 0, p.a.press(t, p.commands)) | |
| 694 | + | p.a.expect(t, "cancelled") | |
| 695 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 696 | + | }) | |
| 697 | + | t.Run("icon with another role", func(t *testing.T) { | |
| 698 | + | p := newDndPair(t) | |
| 699 | + | serial := p.a.press(t, p.commands) | |
| 700 | + | src := p.a.source(t, actCopy, "text/plain") | |
| 701 | + | // The origin toplevel is a role already. | |
| 702 | + | requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestStartDrag, src, p.a.surf, p.a.surf, serial) | |
| 703 | + | expectProtocolError(t, p.a.c, p.a.device.ID(), uint32(wayland.DataDeviceErrorRole)) | |
| 704 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |
| 705 | + | }) | |
| 706 | + | } | |
| 707 | + | ||
| 708 | + | // A nil source is a drag internal to the origin client: only its own | |
| 709 | + | // surfaces see it, without offer. | |
| 710 | + | func TestDataDragWithoutSource(t *testing.T) { | |
| 711 | + | p := newDndPair(t) | |
| 712 | + | p.a.startDrag(t, 0, 0, p.a.press(t, p.commands)) | |
| 713 | + | dndNextEvent[ports.DataDragStarted](t, p.events) | |
| 714 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |
| 715 | + | p.b.none(t) | |
| 716 | + | p.commands <- ports.DataDragFocus{ID: p.a.window, X: 4, Y: 5} | |
| 717 | + | if ev := p.a.expect(t, "enter"); ev.n != 0 || ev.x != 4 || ev.y != 5 { | |
| 718 | + | t.Fatalf("enter %+v", ev) | |
| 719 | + | } | |
| 720 | + | p.commands <- ports.DataDragDrop{} | |
| 721 | + | p.a.expect(t, "drop") | |
| 722 | + | } | |
| 723 | + | ||
| 724 | + | // Offer and source requests are validated. | |
| 725 | + | func TestDataDragProtocolErrors(t *testing.T) { | |
| 726 | + | t.Run("finish before drop", func(t *testing.T) { | |
| 727 | + | p := newDndPair(t) | |
| 728 | + | p.start(t, 0) | |
| 729 | + | offer := p.enter(t) | |
| 730 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish) | |
| 731 | + | expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidFinish)) | |
| 732 | + | }) | |
| 733 | + | t.Run("offer action mask", func(t *testing.T) { | |
| 734 | + | p := newDndPair(t) | |
| 735 | + | p.start(t, 0) | |
| 736 | + | offer := p.enter(t) | |
| 737 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, uint32(8), uint32(0)) | |
| 738 | + | expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidActionMask)) | |
| 739 | + | }) | |
| 740 | + | t.Run("offer preferred action", func(t *testing.T) { | |
| 741 | + | p := newDndPair(t) | |
| 742 | + | p.start(t, 0) | |
| 743 | + | offer := p.enter(t) | |
| 744 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actMove) | |
| 745 | + | expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidAction)) | |
| 746 | + | }) | |
| 747 | + | t.Run("source action mask", func(t *testing.T) { | |
| 748 | + | p := newDndPair(t) | |
| 749 | + | src := p.a.source(t, 0, "text/plain") | |
| 750 | + | requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, uint32(16)) | |
| 751 | + | expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidActionMask)) | |
| 752 | + | }) | |
| 753 | + | t.Run("selection with actions", func(t *testing.T) { | |
| 754 | + | p := newDndPair(t) | |
| 755 | + | src := p.a.source(t, actCopy, "text/plain") | |
| 756 | + | requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestSetSelection, src, uint32(0)) | |
| 757 | + | expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource)) | |
| 758 | + | }) | |
| 759 | + | t.Run("actions after the drag started", func(t *testing.T) { | |
| 760 | + | p := newDndPair(t) | |
| 761 | + | src := p.start(t, 0) | |
| 762 | + | requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, actCopy) | |
| 763 | + | expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource)) | |
| 764 | + | }) | |
| 765 | + | } | |
| 766 | + | ||
| 767 | + | // A destroyed offer before finish cancels the source. | |
| 768 | + | func TestDataDragOfferDestroyedBeforeFinish(t *testing.T) { | |
| 769 | + | p := newDndPair(t) | |
| 770 | + | p.start(t, 0) | |
| 771 | + | offer := p.enter(t) | |
| 772 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |
| 773 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy) | |
| 774 | + | p.a.expect(t, "target") | |
| 775 | + | p.a.expect(t, "action") | |
| 776 | + | p.commands <- ports.DataDragDrop{} | |
| 777 | + | p.b.expect(t, "offer_action") | |
| 778 | + | p.b.expect(t, "drop") | |
| 779 | + | p.b.expect(t, "leave") | |
| 780 | + | p.a.expect(t, "drop_performed") | |
| 781 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy) | |
| 782 | + | p.a.expect(t, "cancelled") | |
| 783 | + | } | |
| 784 | + | ||
| 785 | + | // iconContent reports whether the server still holds content for id. | |
| 786 | + | func iconContent(t *testing.T, s *Server, id ports.WindowID) bool { | |
| 787 | + | t.Helper() | |
| 788 | + | var ok bool | |
| 789 | + | if !s.display.Do(func() { _, ok = s.latest[id] }) { | |
| 790 | + | t.Fatal("display stopped") | |
| 791 | + | } | |
| 792 | + | return ok | |
| 793 | + | } | |
| 794 | + | ||
| 795 | + | // The icon is a surface with its own window ID: its size and the | |
| 796 | + | // offsets of its commits reach core, its content is published, and both go | |
| 797 | + | // when the drag ends. | |
| 798 | + | func TestDataDragIcon(t *testing.T) { | |
| 799 | + | p := newDndPair(t) | |
| 800 | + | icon := p.a.surfaceNew(t) | |
| 801 | + | buf := p.a.buffer(t, 8, 6) | |
| 802 | + | // An offset on an ordinary surface is not used. | |
| 803 | + | requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestOffset, int32(5), int32(5)) | |
| 804 | + | requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestCommit) | |
| 805 | + | ||
| 806 | + | src := p.a.source(t, actCopy, "text/plain") | |
| 807 | + | p.a.startDrag(t, src, icon, p.a.press(t, p.commands)) | |
| 808 | + | started := dndNextEvent[ports.DataDragStarted](t, p.events) | |
| 809 | + | if started.Icon == 0 { | |
| 810 | + | t.Fatal("no icon ID") | |
| 811 | + | } | |
| 812 | + | id := started.Icon | |
| 813 | + | if iconContent(t, p.s, id) { | |
| 814 | + | t.Fatal("content before the first commit") | |
| 815 | + | } | |
| 816 | + | ||
| 817 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 818 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-3), int32(-4)) | |
| 819 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |
| 820 | + | if err := p.a.c.Roundtrip(); err != nil { | |
| 821 | + | t.Fatal(err) | |
| 822 | + | } | |
| 823 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -3, Y: -4, W: 8, H: 6}) { | |
| 824 | + | t.Fatalf("icon %+v", got) | |
| 825 | + | } | |
| 826 | + | if !iconContent(t, p.s, id) { | |
| 827 | + | t.Fatal("no icon content") | |
| 828 | + | } | |
| 829 | + | ||
| 830 | + | // Offsets add up from commit to commit. | |
| 831 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(2)) | |
| 832 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |
| 833 | + | if err := p.a.c.Roundtrip(); err != nil { | |
| 834 | + | t.Fatal(err) | |
| 835 | + | } | |
| 836 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -4, Y: -2, W: 8, H: 6}) { | |
| 837 | + | t.Fatalf("icon %+v", got) | |
| 838 | + | } | |
| 839 | + | // The icon never takes input. | |
| 840 | + | dndNoEvent[ports.InputRegionChanged](t, p.events) | |
| 841 | + | ||
| 842 | + | p.commands <- ports.DataDragCancel{} | |
| 843 | + | p.a.expect(t, "cancelled") | |
| 844 | + | if iconContent(t, p.s, id) { | |
| 845 | + | t.Fatal("icon content kept after the drag") | |
| 846 | + | } | |
| 847 | + | } | |
| 848 | + | ||
| 849 | + | // An icon committed before start_drag shows at once. | |
| 850 | + | func TestDataDragIconCommittedEarly(t *testing.T) { | |
| 851 | + | p := newDndPair(t) | |
| 852 | + | icon := p.a.surfaceNew(t) | |
| 853 | + | buf := p.a.buffer(t, 4, 4) | |
| 854 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 855 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |
| 856 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands)) | |
| 857 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |
| 858 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, W: 4, H: 4}) { | |
| 859 | + | t.Fatalf("icon %+v", got) | |
| 860 | + | } | |
| 861 | + | if !iconContent(t, p.s, id) { | |
| 862 | + | t.Fatal("no icon content") | |
| 863 | + | } | |
| 864 | + | } | |
| 865 | + | ||
| 866 | + | // Before wl_surface v5 the icon offset rides on attach and adds up. | |
| 867 | + | func TestDataDragIconAttachOffset(t *testing.T) { | |
| 868 | + | p := newDndPair(t) | |
| 869 | + | comp4 := bindVersion(t, p.a.c, "wl_compositor", 4) | |
| 870 | + | icon := p.a.c.AllocateID() | |
| 871 | + | requestProtocol(t, p.a.c, comp4, wayland.CompositorRequestCreateSurface, icon) | |
| 872 | + | registerProtocol(t, p.a.c, icon) | |
| 873 | + | buf := p.a.buffer(t, 4, 4) | |
| 874 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands)) | |
| 875 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |
| 876 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(-2), int32(-1)) | |
| 877 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |
| 878 | + | if err := p.a.c.Roundtrip(); err != nil { | |
| 879 | + | t.Fatal(err) | |
| 880 | + | } | |
| 881 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -2, Y: -1, W: 4, H: 4}) { | |
| 882 | + | t.Fatalf("icon %+v", got) | |
| 883 | + | } | |
| 884 | + | } | |
| 885 | + | ||
| 886 | + | // Destroying the icon mid-drag hides it; the drag goes on. | |
| 887 | + | func TestDataDragIconDestroyed(t *testing.T) { | |
| 888 | + | p := newDndPair(t) | |
| 889 | + | icon := p.a.surfaceNew(t) | |
| 890 | + | buf := p.a.buffer(t, 8, 6) | |
| 891 | + | p.a.startDrag(t, p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list"), icon, p.a.press(t, p.commands)) | |
| 892 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |
| 893 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 894 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |
| 895 | + | if err := p.a.c.Roundtrip(); err != nil { | |
| 896 | + | t.Fatal(err) | |
| 897 | + | } | |
| 898 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got.W != 8 { | |
| 899 | + | t.Fatalf("icon %+v", got) | |
| 900 | + | } | |
| 901 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestDestroy) | |
| 902 | + | if err := p.a.c.Roundtrip(); err != nil { | |
| 903 | + | t.Fatal(err) | |
| 904 | + | } | |
| 905 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id}) { | |
| 906 | + | t.Fatalf("icon %+v", got) | |
| 907 | + | } | |
| 908 | + | if iconContent(t, p.s, id) { | |
| 909 | + | t.Fatal("icon content kept") | |
| 910 | + | } | |
| 911 | + | p.enter(t) | |
| 912 | + | } | |
| 913 | + | ||
| 914 | + | // lockedPair is a pair of clients on a server that can lock the session. | |
| 915 | + | func lockedPair(t *testing.T) (*dndPair, string) { | |
| 916 | + | t.Helper() | |
| 917 | + | gate := &sessionsecurity.Gate{} | |
| 918 | + | events := make(chan ports.ClientEvent, 128) | |
| 919 | + | commands := make(chan ports.ClientCommand, 32) | |
| 920 | + | dir := t.TempDir() | |
| 921 | + | 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")) | |
| 922 | + | if err != nil { | |
| 923 | + | t.Fatal(err) | |
| 924 | + | } | |
| 925 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 926 | + | done := make(chan error, 1) | |
| 927 | + | go func() { done <- s.Run(ctx) }() | |
| 928 | + | t.Cleanup(func() { | |
| 929 | + | cancel() | |
| 930 | + | select { | |
| 931 | + | case err := <-done: | |
| 932 | + | if err != nil { | |
| 933 | + | t.Error(err) | |
| 934 | + | } | |
| 935 | + | case <-time.After(3 * time.Second): | |
| 936 | + | t.Error("server did not stop") | |
| 937 | + | } | |
| 938 | + | }) | |
| 939 | + | p := &dndPair{s: s, events: events, commands: commands} | |
| 940 | + | p.a = newDndClient(t, s, dir, events) | |
| 941 | + | p.b = newDndClient(t, s, dir, events) | |
| 942 | + | return p, dir | |
| 943 | + | } | |
| 944 | + | ||
| 945 | + | func lockSession(t *testing.T, s *Server, dir string) { | |
| 946 | + | t.Helper() | |
| 947 | + | lc := protocolClient(t, s, dir) | |
| 948 | + | if _, err := bindLockManager(t, lc).Lock(); err != nil { | |
| 949 | + | t.Fatal(err) | |
| 950 | + | } | |
| 951 | + | if err := lc.Roundtrip(); err != nil { | |
| 952 | + | t.Fatal(err) | |
| 953 | + | } | |
| 954 | + | } | |
| 955 | + | ||
| 956 | + | // A7: the session lock cancels a drag at once: target leave, source | |
| 957 | + | // cancelled, icon gone. | |
| 958 | + | func TestDataDragLockCancels(t *testing.T) { | |
| 959 | + | p, dir := lockedPair(t) | |
| 960 | + | icon := p.a.surfaceNew(t) | |
| 961 | + | buf := p.a.buffer(t, 8, 6) | |
| 962 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 963 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |
| 964 | + | src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list") | |
| 965 | + | p.a.startDrag(t, src, icon, p.a.press(t, p.commands)) | |
| 966 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |
| 967 | + | p.enter(t) | |
| 968 | + | ||
| 969 | + | lockSession(t, p.s, dir) | |
| 970 | + | p.b.expect(t, "leave") | |
| 971 | + | p.a.expect(t, "cancelled") | |
| 972 | + | if iconContent(t, p.s, id) { | |
| 973 | + | t.Fatal("icon content kept") | |
| 974 | + | } | |
| 975 | + | // A new drag is refused while protected. | |
| 976 | + | src2 := p.a.source(t, actCopy, "text/plain") | |
| 977 | + | p.a.startDrag(t, src2, 0, uint32(1)) | |
| 978 | + | p.a.expect(t, "cancelled") | |
| 979 | + | } | |
| 980 | + | ||
| 981 | + | // GO-006: a dropped transfer waiting for its finish dies with the lock, for | |
| 982 | + | // a v3 and a pre-v3 target alike. | |
| 983 | + | func TestDataDropLockCancelsPending(t *testing.T) { | |
| 984 | + | for _, version := range []uint32{3, 2} { | |
| 985 | + | p, dir := lockedPair(t) | |
| 986 | + | p.b = newDndClientAt(t, p.s, dir, p.events, version) | |
| 987 | + | p.start(t, 0) | |
| 988 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6} | |
| 989 | + | p.b.until(t, "enter") | |
| 990 | + | offer := p.b.lastOffer(t) | |
| 991 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |
| 992 | + | if version >= 3 { | |
| 993 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy) | |
| 994 | + | } | |
| 995 | + | p.a.until(t, "target") | |
| 996 | + | p.commands <- ports.DataDragDrop{} | |
| 997 | + | p.a.until(t, "drop_performed") | |
| 998 | + | lockSession(t, p.s, dir) | |
| 999 | + | p.a.expect(t, "cancelled") | |
| 1000 | + | } | |
| 1001 | + | } | |
| 1002 | + | ||
| 1003 | + | // A subsurface of the focused tree is a valid origin. | |
| 1004 | + | func TestDataDragFromSubsurface(t *testing.T) { | |
| 1005 | + | p := newDndPair(t) | |
| 1006 | + | serial := p.a.press(t, p.commands) | |
| 1007 | + | child := p.a.surfaceNew(t) | |
| 1008 | + | subc := bindProtocol(t, p.a.c, "wl_subcompositor") | |
| 1009 | + | requestProtocol(t, p.a.c, subc, wayland.SubcompositorRequestGetSubsurface, p.a.c.AllocateID(), child, p.a.surf) | |
| 1010 | + | p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), child, 0, serial) | |
| 1011 | + | dndNextEvent[ports.DataDragStarted](t, p.events) | |
| 1012 | + | } | |
| 1013 | + | ||
| 1014 | + | // GO-003: destroying the offers of the target withdraws the acceptance: the | |
| 1015 | + | // release then cancels, with no drop. | |
| 1016 | + | func TestDataDragOfferDestroyedBeforeRelease(t *testing.T) { | |
| 1017 | + | p := newDndPair(t) | |
| 1018 | + | p.start(t, 0) | |
| 1019 | + | offer := p.enter(t) | |
| 1020 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |
| 1021 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy) | |
| 1022 | + | p.a.expect(t, "target") | |
| 1023 | + | p.a.expect(t, "action") | |
| 1024 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy) | |
| 1025 | + | if err := p.b.c.Roundtrip(); err != nil { | |
| 1026 | + | t.Fatal(err) | |
| 1027 | + | } | |
| 1028 | + | if ev := p.a.expect(t, "target"); ev.mime != "" { | |
| 1029 | + | t.Fatalf("target %+v", ev) | |
| 1030 | + | } | |
| 1031 | + | if ev := p.a.expect(t, "action"); ev.n != 0 { | |
| 1032 | + | t.Fatalf("action %+v", ev) | |
| 1033 | + | } | |
| 1034 | + | p.commands <- ports.DataDragDrop{} | |
| 1035 | + | got := p.b.until(t, "leave") | |
| 1036 | + | for _, ev := range got { | |
| 1037 | + | if ev.name == "drop" { | |
| 1038 | + | t.Fatalf("drop after the offer was destroyed: %+v", got) | |
| 1039 | + | } | |
| 1040 | + | } | |
| 1041 | + | p.a.expect(t, "cancelled") | |
| 1042 | + | } | |
| 1043 | + | ||
| 1044 | + | // GO-004: with ask, the target picks the final action after the drop; the | |
| 1045 | + | // source hears it just before dnd_finished. | |
| 1046 | + | func TestDataDragAsk(t *testing.T) { | |
| 1047 | + | p := newDndPair(t) | |
| 1048 | + | src := p.a.source(t, actCopy|actMove|actAsk, "text/plain") | |
| 1049 | + | p.a.startDrag(t, src, 0, p.a.press(t, p.commands)) | |
| 1050 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |
| 1051 | + | p.b.until(t, "enter") | |
| 1052 | + | id := p.b.lastOffer(t) | |
| 1053 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |
| 1054 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk) | |
| 1055 | + | p.a.expect(t, "target") | |
| 1056 | + | if ev := p.a.expect(t, "action"); ev.n != actAsk { | |
| 1057 | + | t.Fatalf("action %+v", ev) | |
| 1058 | + | } | |
| 1059 | + | p.commands <- ports.DataDragDrop{} | |
| 1060 | + | p.b.until(t, "leave") | |
| 1061 | + | p.a.expect(t, "drop_performed") | |
| 1062 | + | ||
| 1063 | + | // Finishing while the action is ask is an error; the choice comes first. | |
| 1064 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(2), "text/plain") | |
| 1065 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove) | |
| 1066 | + | p.a.expect(t, "target") | |
| 1067 | + | p.b.none(t) // no offer.action after the drop | |
| 1068 | + | p.a.none(t) // the final action waits for the finish | |
| 1069 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestFinish) | |
| 1070 | + | if ev := p.a.expect(t, "action"); ev.n != actMove { | |
| 1071 | + | t.Fatalf("action %+v", ev) | |
| 1072 | + | } | |
| 1073 | + | p.a.expect(t, "finished") | |
| 1074 | + | } | |
| 1075 | + | ||
| 1076 | + | // A final preferred action outside the source's is an error. | |
| 1077 | + | func TestDataDragAskFinalActionNotInSource(t *testing.T) { | |
| 1078 | + | p := newDndPair(t) | |
| 1079 | + | src := p.a.source(t, actCopy|actAsk, "text/plain") | |
| 1080 | + | p.a.startDrag(t, src, 0, p.a.press(t, p.commands)) | |
| 1081 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |
| 1082 | + | p.b.until(t, "enter") | |
| 1083 | + | id := p.b.lastOffer(t) | |
| 1084 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |
| 1085 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk) | |
| 1086 | + | p.a.expect(t, "target") | |
| 1087 | + | p.a.expect(t, "action") | |
| 1088 | + | p.commands <- ports.DataDragDrop{} | |
| 1089 | + | p.b.until(t, "leave") | |
| 1090 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actMove, actMove) | |
| 1091 | + | expectProtocolError(t, p.b.c, id, uint32(wayland.DataOfferErrorInvalidAction)) | |
| 1092 | + | } | |
| 1093 | + | ||
| 1094 | + | // GO-005: a pre-v3 target copies: a source that does not allow copy is | |
| 1095 | + | // cancelled, one that does gets it. | |
| 1096 | + | func TestDataDragLegacyTarget(t *testing.T) { | |
| 1097 | + | setup := func(t *testing.T, actions uint32) (*dndPair, uint32) { | |
| 1098 | + | s, events, commands, dir := keyboardServer(t) | |
| 1099 | + | p := &dndPair{s: s, events: events, commands: commands} | |
| 1100 | + | p.a = newDndClient(t, s, dir, events) | |
| 1101 | + | p.b = newDndClientAt(t, s, dir, events, 2) | |
| 1102 | + | src := p.a.source(t, actions, "text/plain") | |
| 1103 | + | p.a.startDrag(t, src, 0, p.a.press(t, p.commands)) | |
| 1104 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |
| 1105 | + | p.b.until(t, "enter") | |
| 1106 | + | id := p.b.lastOffer(t) | |
| 1107 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |
| 1108 | + | p.a.expect(t, "target") | |
| 1109 | + | return p, id | |
| 1110 | + | } | |
| 1111 | + | t.Run("move only", func(t *testing.T) { | |
| 1112 | + | p, _ := setup(t, actMove) | |
| 1113 | + | p.commands <- ports.DataDragDrop{} | |
| 1114 | + | got := p.b.until(t, "leave") | |
| 1115 | + | for _, ev := range got { | |
| 1116 | + | if ev.name == "drop" { | |
| 1117 | + | t.Fatalf("drop for a source without copy: %+v", got) | |
| 1118 | + | } | |
| 1119 | + | } | |
| 1120 | + | p.a.until(t, "cancelled") | |
| 1121 | + | }) | |
| 1122 | + | t.Run("copy", func(t *testing.T) { | |
| 1123 | + | p, id := setup(t, actCopy|actMove) | |
| 1124 | + | p.commands <- ports.DataDragDrop{} | |
| 1125 | + | p.b.expect(t, "drop") | |
| 1126 | + | p.b.expect(t, "leave") | |
| 1127 | + | if ev := p.a.expect(t, "action"); ev.n != actCopy { | |
| 1128 | + | t.Fatalf("action %+v", ev) | |
| 1129 | + | } | |
| 1130 | + | p.a.expect(t, "drop_performed") | |
| 1131 | + | // GO-006: destroying the last offer finishes the transfer. | |
| 1132 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestDestroy) | |
| 1133 | + | p.a.expect(t, "finished") | |
| 1134 | + | }) | |
| 1135 | + | } | |
| 1136 | + | ||
| 1137 | + | // GO-007: of several offset requests before one commit, the last counts. | |
| 1138 | + | func TestDataDragIconOffsetReplacedBeforeCommit(t *testing.T) { | |
| 1139 | + | p := newDndPair(t) | |
| 1140 | + | icon := p.a.surfaceNew(t) | |
| 1141 | + | buf := p.a.buffer(t, 8, 6) | |
| 1142 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands)) | |
| 1143 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |
| 1144 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 1145 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(-1)) | |
| 1146 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-5), int32(-7)) | |
| 1147 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |
| 1148 | + | if err := p.a.c.Roundtrip(); err != nil { | |
| 1149 | + | t.Fatal(err) | |
| 1150 | + | } | |
| 1151 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -5, Y: -7, W: 8, H: 6}) { | |
| 1152 | + | t.Fatalf("icon %+v", got) | |
| 1153 | + | } | |
| 1154 | + | } | |
| @@ -0,0 +1,1154 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "sync/atomic" | |||
| 7 | + | "testing" | |||
| 8 | + | "time" | |||
| 9 | + | ||||
| 10 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 11 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |||
| 12 | + | "github.com/bnema/neferwl/internal/adapters/sessionsecurity" | |||
| 13 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 14 | + | "github.com/bnema/wlturbo" | |||
| 15 | + | "golang.org/x/sys/unix" | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | const ( | |||
| 19 | + | actCopy = uint32(wayland.DataDeviceManagerDndActionCopy) | |||
| 20 | + | actMove = uint32(wayland.DataDeviceManagerDndActionMove) | |||
| 21 | + | actAsk = uint32(wayland.DataDeviceManagerDndActionAsk) | |||
| 22 | + | ) | |||
| 23 | + | ||||
| 24 | + | // dndEvent is one data device, offer or source event a test client saw. | |||
| 25 | + | type dndEvent struct { | |||
| 26 | + | name string | |||
| 27 | + | mime string | |||
| 28 | + | x, y float64 | |||
| 29 | + | n uint32 | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | type dndDeviceEvents struct { | |||
| 33 | + | wlturbo.BaseProxy | |||
| 34 | + | c *wlturbo.Display | |||
| 35 | + | log chan dndEvent | |||
| 36 | + | // offer is the id of the latest data_offer. | |||
| 37 | + | offer atomic.Uint32 | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | func (p *dndDeviceEvents) Dispatch(e *wlturbo.Event) { | |||
| 41 | + | switch e.Opcode { | |||
| 42 | + | case uint16(wayland.DataDeviceEventDataOffer): | |||
| 43 | + | o := &dndOfferEvents{log: p.log} | |||
| 44 | + | o.SetID(e.Uint32()) | |||
| 45 | + | p.offer.Store(o.ID()) | |||
| 46 | + | registerWireProxy(p.c, o) | |||
| 47 | + | case uint16(wayland.DataDeviceEventEnter): | |||
| 48 | + | serial, _ := e.Uint32(), e.Uint32() | |||
| 49 | + | x, y := e.Fixed().Float64(), e.Fixed().Float64() | |||
| 50 | + | p.log <- dndEvent{name: "enter", x: x, y: y, n: e.Uint32() + serial*0} | |||
| 51 | + | case uint16(wayland.DataDeviceEventLeave): | |||
| 52 | + | p.log <- dndEvent{name: "leave"} | |||
| 53 | + | case uint16(wayland.DataDeviceEventMotion): | |||
| 54 | + | _ = e.Uint32() | |||
| 55 | + | p.log <- dndEvent{name: "motion", x: e.Fixed().Float64(), y: e.Fixed().Float64()} | |||
| 56 | + | case uint16(wayland.DataDeviceEventDrop): | |||
| 57 | + | p.log <- dndEvent{name: "drop"} | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | type dndOfferEvents struct { | |||
| 62 | + | wlturbo.BaseProxy | |||
| 63 | + | log chan dndEvent | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | func (p *dndOfferEvents) Dispatch(e *wlturbo.Event) { | |||
| 67 | + | switch e.Opcode { | |||
| 68 | + | case uint16(wayland.DataOfferEventOffer): | |||
| 69 | + | p.log <- dndEvent{name: "offer", mime: e.String()} | |||
| 70 | + | case uint16(wayland.DataOfferEventSourceActions): | |||
| 71 | + | p.log <- dndEvent{name: "source_actions", n: e.Uint32()} | |||
| 72 | + | case uint16(wayland.DataOfferEventAction): | |||
| 73 | + | p.log <- dndEvent{name: "offer_action", n: e.Uint32()} | |||
| 74 | + | } | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | type dndSourceEvents struct { | |||
| 78 | + | wlturbo.BaseProxy | |||
| 79 | + | log chan dndEvent | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | func (p *dndSourceEvents) Dispatch(e *wlturbo.Event) { | |||
| 83 | + | switch e.Opcode { | |||
| 84 | + | case uint16(wayland.DataSourceEventTarget): | |||
| 85 | + | p.log <- dndEvent{name: "target", mime: e.String()} | |||
| 86 | + | case uint16(wayland.DataSourceEventSend): | |||
| 87 | + | mime := e.String() | |||
| 88 | + | fd := int(e.Fd()) | |||
| 89 | + | _, _ = unix.Write(fd, []byte("dnd data")) | |||
| 90 | + | _ = unix.Close(fd) | |||
| 91 | + | p.log <- dndEvent{name: "send", mime: mime} | |||
| 92 | + | case uint16(wayland.DataSourceEventCancelled): | |||
| 93 | + | p.log <- dndEvent{name: "cancelled"} | |||
| 94 | + | case uint16(wayland.DataSourceEventDndDropPerformed): | |||
| 95 | + | p.log <- dndEvent{name: "drop_performed"} | |||
| 96 | + | case uint16(wayland.DataSourceEventDndFinished): | |||
| 97 | + | p.log <- dndEvent{name: "finished"} | |||
| 98 | + | case uint16(wayland.DataSourceEventAction): | |||
| 99 | + | p.log <- dndEvent{name: "action", n: e.Uint32()} | |||
| 100 | + | } | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | // dndClient is a client with a toplevel, a pointer and a data device. | |||
| 104 | + | type dndClient struct { | |||
| 105 | + | keys chan uint32 | |||
| 106 | + | c *wlturbo.Display | |||
| 107 | + | manager uint32 | |||
| 108 | + | device *dndDeviceEvents | |||
| 109 | + | serials chan uint32 | |||
| 110 | + | comp uint32 | |||
| 111 | + | shm uint32 | |||
| 112 | + | surf uint32 | |||
| 113 | + | window ports.WindowID | |||
| 114 | + | log chan dndEvent | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | // keySerials records the serial of each key event. | |||
| 118 | + | type keySerials struct { | |||
| 119 | + | wlturbo.BaseProxy | |||
| 120 | + | serials chan uint32 | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | func (p *keySerials) Dispatch(e *wlturbo.Event) { | |||
| 124 | + | if e.Opcode == uint16(wayland.KeyboardEventKey) { | |||
| 125 | + | p.serials <- e.Uint32() | |||
| 126 | + | } | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | func newDndClient(t *testing.T, s *Server, dir string, events <-chan ports.ClientEvent) *dndClient { | |||
| 130 | + | t.Helper() | |||
| 131 | + | return newDndClientAt(t, s, dir, events, 3) | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | // newDndClientAt is newDndClient with wl_data_device_manager at version. | |||
| 135 | + | func newDndClientAt(t *testing.T, s *Server, dir string, events <-chan ports.ClientEvent, version uint32) *dndClient { | |||
| 136 | + | t.Helper() | |||
| 137 | + | c := protocolClient(t, s, dir) | |||
| 138 | + | d := &dndClient{c: c, log: make(chan dndEvent, 64), serials: make(chan uint32, 8), keys: make(chan uint32, 8)} | |||
| 139 | + | kb := &keySerials{serials: d.keys} | |||
| 140 | + | seat := bindVersion(t, c, "wl_seat", 8) | |||
| 141 | + | registerProtocol(t, c, seat) | |||
| 142 | + | ptr := &pressSerials{serials: d.serials} | |||
| 143 | + | ptr.SetID(c.AllocateID()) | |||
| 144 | + | registerWireProxy(c, ptr) | |||
| 145 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, ptr.ID()) | |||
| 146 | + | kb.SetID(c.AllocateID()) | |||
| 147 | + | registerWireProxy(c, kb) | |||
| 148 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, kb.ID()) | |||
| 149 | + | d.manager = bindVersion(t, c, "wl_data_device_manager", version) | |||
| 150 | + | d.device = &dndDeviceEvents{c: c, log: d.log} | |||
| 151 | + | d.device.SetID(c.AllocateID()) | |||
| 152 | + | registerWireProxy(c, d.device) | |||
| 153 | + | requestProtocol(t, c, d.manager, wayland.DataDeviceManagerRequestGetDataDevice, d.device.ID(), seat) | |||
| 154 | + | d.comp = bindVersion(t, c, "wl_compositor", 5) | |||
| 155 | + | d.shm = bindProtocol(t, c, "wl_shm") | |||
| 156 | + | registerProtocol(t, c, d.shm) | |||
| 157 | + | w, surf, _ := surfaceMapperAt(t, c, events, 5)() | |||
| 158 | + | d.window, d.surf = w.ID, surf | |||
| 159 | + | return d | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | // lastOffer is the id of the latest offer the client got. | |||
| 163 | + | func (d *dndClient) lastOffer(t *testing.T) uint32 { | |||
| 164 | + | t.Helper() | |||
| 165 | + | id := d.device.offer.Load() | |||
| 166 | + | if id == 0 { | |||
| 167 | + | t.Fatal("no offer") | |||
| 168 | + | } | |||
| 169 | + | return id | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | // next returns the next event the client sees. | |||
| 173 | + | func (d *dndClient) next(t *testing.T) dndEvent { | |||
| 174 | + | t.Helper() | |||
| 175 | + | deadline := time.After(2 * time.Second) | |||
| 176 | + | for { | |||
| 177 | + | if err := d.c.Roundtrip(); err != nil { | |||
| 178 | + | t.Fatal(err) | |||
| 179 | + | } | |||
| 180 | + | select { | |||
| 181 | + | case ev := <-d.log: | |||
| 182 | + | return ev | |||
| 183 | + | case <-deadline: | |||
| 184 | + | t.Fatal("no event") | |||
| 185 | + | case <-time.After(5 * time.Millisecond): | |||
| 186 | + | } | |||
| 187 | + | } | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | // expect waits for the next event and checks its name. | |||
| 191 | + | func (d *dndClient) expect(t *testing.T, name string) dndEvent { | |||
| 192 | + | t.Helper() | |||
| 193 | + | ev := d.next(t) | |||
| 194 | + | if ev.name != name { | |||
| 195 | + | t.Fatalf("event %+v, want %s", ev, name) | |||
| 196 | + | } | |||
| 197 | + | return ev | |||
| 198 | + | } | |||
| 199 | + | ||||
| 200 | + | // none checks that no event arrives. | |||
| 201 | + | func (d *dndClient) none(t *testing.T) { | |||
| 202 | + | t.Helper() | |||
| 203 | + | for range 2 { | |||
| 204 | + | if err := d.c.Roundtrip(); err != nil { | |||
| 205 | + | t.Fatal(err) | |||
| 206 | + | } | |||
| 207 | + | time.Sleep(30 * time.Millisecond) | |||
| 208 | + | } | |||
| 209 | + | select { | |||
| 210 | + | case ev := <-d.log: | |||
| 211 | + | t.Fatalf("unexpected event %+v", ev) | |||
| 212 | + | default: | |||
| 213 | + | } | |||
| 214 | + | } | |||
| 215 | + | ||||
| 216 | + | func (d *dndClient) source(t *testing.T, actions uint32, mimes ...string) uint32 { | |||
| 217 | + | t.Helper() | |||
| 218 | + | src := &dndSourceEvents{log: d.log} | |||
| 219 | + | src.SetID(d.c.AllocateID()) | |||
| 220 | + | registerWireProxy(d.c, src) | |||
| 221 | + | requestProtocol(t, d.c, d.manager, wayland.DataDeviceManagerRequestCreateDataSource, src.ID()) | |||
| 222 | + | for _, m := range mimes { | |||
| 223 | + | requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestOffer, m) | |||
| 224 | + | } | |||
| 225 | + | if actions != 0 { | |||
| 226 | + | requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestSetActions, actions) | |||
| 227 | + | } | |||
| 228 | + | return src.ID() | |||
| 229 | + | } | |||
| 230 | + | ||||
| 231 | + | // press makes the client's window press a button and returns the serial. | |||
| 232 | + | func (d *dndClient) press(t *testing.T, commands chan<- ports.ClientCommand) uint32 { | |||
| 233 | + | t.Helper() | |||
| 234 | + | commands <- ports.PointerFocus{ID: d.window, X: 1, Y: 1} | |||
| 235 | + | commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Pressed: true, Grab: true, Time: time.Millisecond} | |||
| 236 | + | deadline := time.After(2 * time.Second) | |||
| 237 | + | for { | |||
| 238 | + | if err := d.c.Roundtrip(); err != nil { | |||
| 239 | + | t.Fatal(err) | |||
| 240 | + | } | |||
| 241 | + | select { | |||
| 242 | + | case serial := <-d.serials: | |||
| 243 | + | return serial | |||
| 244 | + | case <-deadline: | |||
| 245 | + | t.Fatal("no button") | |||
| 246 | + | case <-time.After(5 * time.Millisecond): | |||
| 247 | + | } | |||
| 248 | + | } | |||
| 249 | + | } | |||
| 250 | + | ||||
| 251 | + | // press2 returns the next pointer button serial. | |||
| 252 | + | func (d *dndClient) press2(t *testing.T) uint32 { | |||
| 253 | + | t.Helper() | |||
| 254 | + | deadline := time.After(2 * time.Second) | |||
| 255 | + | for { | |||
| 256 | + | if err := d.c.Roundtrip(); err != nil { | |||
| 257 | + | t.Fatal(err) | |||
| 258 | + | } | |||
| 259 | + | select { | |||
| 260 | + | case serial := <-d.serials: | |||
| 261 | + | return serial | |||
| 262 | + | case <-deadline: | |||
| 263 | + | t.Fatal("no button") | |||
| 264 | + | case <-time.After(5 * time.Millisecond): | |||
| 265 | + | } | |||
| 266 | + | } | |||
| 267 | + | } | |||
| 268 | + | ||||
| 269 | + | // release releases the button press took and waits for the client to see it. | |||
| 270 | + | func (d *dndClient) release(t *testing.T, commands chan<- ports.ClientCommand) { | |||
| 271 | + | t.Helper() | |||
| 272 | + | commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Time: 2 * time.Millisecond} | |||
| 273 | + | d.press2(t) | |||
| 274 | + | } | |||
| 275 | + | ||||
| 276 | + | // key presses a key on the client's focused window and returns the serial. | |||
| 277 | + | func (d *dndClient) key(t *testing.T, commands chan<- ports.ClientCommand) uint32 { | |||
| 278 | + | t.Helper() | |||
| 279 | + | commands <- ports.FocusWindow{ID: d.window} | |||
| 280 | + | commands <- ports.ForwardKey{ID: d.window, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 3 * time.Millisecond}} | |||
| 281 | + | deadline := time.After(2 * time.Second) | |||
| 282 | + | for { | |||
| 283 | + | if err := d.c.Roundtrip(); err != nil { | |||
| 284 | + | t.Fatal(err) | |||
| 285 | + | } | |||
| 286 | + | select { | |||
| 287 | + | case serial := <-d.keys: | |||
| 288 | + | return serial | |||
| 289 | + | case <-deadline: | |||
| 290 | + | t.Fatal("no key") | |||
| 291 | + | case <-time.After(5 * time.Millisecond): | |||
| 292 | + | } | |||
| 293 | + | } | |||
| 294 | + | } | |||
| 295 | + | ||||
| 296 | + | // events returns what the client saw up to and including name. | |||
| 297 | + | func (d *dndClient) until(t *testing.T, name string) []dndEvent { | |||
| 298 | + | t.Helper() | |||
| 299 | + | var got []dndEvent | |||
| 300 | + | for { | |||
| 301 | + | ev := d.next(t) | |||
| 302 | + | got = append(got, ev) | |||
| 303 | + | if ev.name == name { | |||
| 304 | + | return got | |||
| 305 | + | } | |||
| 306 | + | } | |||
| 307 | + | } | |||
| 308 | + | ||||
| 309 | + | func (d *dndClient) startDrag(t *testing.T, src, icon, serial uint32) { | |||
| 310 | + | t.Helper() | |||
| 311 | + | d.startDragFrom(t, src, d.surf, icon, serial) | |||
| 312 | + | } | |||
| 313 | + | ||||
| 314 | + | // startDragFrom is startDrag with an origin surface. | |||
| 315 | + | func (d *dndClient) startDragFrom(t *testing.T, src, origin, icon, serial uint32) { | |||
| 316 | + | t.Helper() | |||
| 317 | + | requestProtocol(t, d.c, d.device.ID(), wayland.DataDeviceRequestStartDrag, src, origin, icon, serial) | |||
| 318 | + | if err := d.c.Roundtrip(); err != nil { | |||
| 319 | + | t.Fatal(err) | |||
| 320 | + | } | |||
| 321 | + | } | |||
| 322 | + | ||||
| 323 | + | // buffer makes a w×h ARGB shm buffer. | |||
| 324 | + | func (d *dndClient) buffer(t *testing.T, w, h int) uint32 { | |||
| 325 | + | t.Helper() | |||
| 326 | + | fd, err := unix.MemfdCreate("dnd-icon", 0) | |||
| 327 | + | if err != nil { | |||
| 328 | + | t.Fatal(err) | |||
| 329 | + | } | |||
| 330 | + | defer unix.Close(fd) | |||
| 331 | + | if err := unix.Ftruncate(fd, int64(w*h*4)); err != nil { | |||
| 332 | + | t.Fatal(err) | |||
| 333 | + | } | |||
| 334 | + | pool, buf := d.c.AllocateID(), d.c.AllocateID() | |||
| 335 | + | registerProtocol(t, d.c, pool) | |||
| 336 | + | registerProtocol(t, d.c, buf) | |||
| 337 | + | if err := wireRequest(d.c, d.shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(w*h*4)); err != nil { | |||
| 338 | + | t.Fatal(err) | |||
| 339 | + | } | |||
| 340 | + | requestProtocol(t, d.c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(w), int32(h), int32(w*4), uint32(0)) | |||
| 341 | + | return buf | |||
| 342 | + | } | |||
| 343 | + | ||||
| 344 | + | func (d *dndClient) surfaceNew(t *testing.T) uint32 { | |||
| 345 | + | t.Helper() | |||
| 346 | + | id := d.c.AllocateID() | |||
| 347 | + | requestProtocol(t, d.c, d.comp, wayland.CompositorRequestCreateSurface, id) | |||
| 348 | + | registerProtocol(t, d.c, id) | |||
| 349 | + | return id | |||
| 350 | + | } | |||
| 351 | + | ||||
| 352 | + | // dndNextEvent returns the next client event of type T, skipping others. | |||
| 353 | + | func dndNextEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T { | |||
| 354 | + | t.Helper() | |||
| 355 | + | deadline := time.After(2 * time.Second) | |||
| 356 | + | for { | |||
| 357 | + | select { | |||
| 358 | + | case ev := <-events: | |||
| 359 | + | if v, ok := ev.(T); ok { | |||
| 360 | + | return v | |||
| 361 | + | } | |||
| 362 | + | case <-deadline: | |||
| 363 | + | t.Fatalf("no %T event", *new(T)) | |||
| 364 | + | } | |||
| 365 | + | } | |||
| 366 | + | } | |||
| 367 | + | ||||
| 368 | + | // dndNoEvent checks that no client event of type T is pending. | |||
| 369 | + | func dndNoEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) { | |||
| 370 | + | t.Helper() | |||
| 371 | + | time.Sleep(50 * time.Millisecond) | |||
| 372 | + | for { | |||
| 373 | + | select { | |||
| 374 | + | case ev := <-events: | |||
| 375 | + | if _, ok := ev.(T); ok { | |||
| 376 | + | t.Fatalf("unexpected %T: %+v", ev, ev) | |||
| 377 | + | } | |||
| 378 | + | default: | |||
| 379 | + | return | |||
| 380 | + | } | |||
| 381 | + | } | |||
| 382 | + | } | |||
| 383 | + | ||||
| 384 | + | // dndPair is two clients with a drag started from a on a source. | |||
| 385 | + | type dndPair struct { | |||
| 386 | + | s *Server | |||
| 387 | + | events chan ports.ClientEvent | |||
| 388 | + | commands chan ports.ClientCommand | |||
| 389 | + | a, b *dndClient | |||
| 390 | + | } | |||
| 391 | + | ||||
| 392 | + | func newDndPair(t *testing.T) *dndPair { | |||
| 393 | + | t.Helper() | |||
| 394 | + | s, events, commands, dir := keyboardServer(t) | |||
| 395 | + | p := &dndPair{s: s, events: events, commands: commands} | |||
| 396 | + | p.a = newDndClient(t, s, dir, events) | |||
| 397 | + | p.b = newDndClient(t, s, dir, events) | |||
| 398 | + | return p | |||
| 399 | + | } | |||
| 400 | + | ||||
| 401 | + | // start begins a drag of a source offering text/plain, copy and move. | |||
| 402 | + | func (p *dndPair) start(t *testing.T, icon uint32) uint32 { | |||
| 403 | + | t.Helper() | |||
| 404 | + | src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list") | |||
| 405 | + | p.a.startDrag(t, src, icon, p.a.press(t, p.commands)) | |||
| 406 | + | return src | |||
| 407 | + | } | |||
| 408 | + | ||||
| 409 | + | // enter makes b the drag target and returns the offer id. | |||
| 410 | + | func (p *dndPair) enter(t *testing.T) uint32 { | |||
| 411 | + | t.Helper() | |||
| 412 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6, Time: 10 * time.Millisecond} | |||
| 413 | + | for _, want := range []dndEvent{{name: "offer", mime: "text/plain"}, {name: "offer", mime: "text/uri-list"}, {name: "source_actions", n: actCopy | actMove}} { | |||
| 414 | + | if ev := p.b.next(t); ev != want { | |||
| 415 | + | t.Fatalf("event %+v, want %+v", ev, want) | |||
| 416 | + | } | |||
| 417 | + | } | |||
| 418 | + | ev := p.b.expect(t, "enter") | |||
| 419 | + | if ev.x != 5 || ev.y != 6 || ev.n == 0 { | |||
| 420 | + | t.Fatalf("enter %+v", ev) | |||
| 421 | + | } | |||
| 422 | + | return ev.n | |||
| 423 | + | } | |||
| 424 | + | ||||
| 425 | + | // A1: a drag from a to b delivers enter, motion and drop with the offer; | |||
| 426 | + | // the action is negotiated; the data reaches b; a is told every step. | |||
| 427 | + | func TestDataDragBetweenClients(t *testing.T) { | |||
| 428 | + | p := newDndPair(t) | |||
| 429 | + | p.start(t, 0) | |||
| 430 | + | if v := dndNextEvent[ports.DataDragStarted](t, p.events); v.Icon != 0 { | |||
| 431 | + | t.Fatalf("started %+v", v) | |||
| 432 | + | } | |||
| 433 | + | offer := p.enter(t) | |||
| 434 | + | ||||
| 435 | + | p.commands <- ports.DataDragMotion{ID: p.b.window, X: 7, Y: 8, Time: 20 * time.Millisecond} | |||
| 436 | + | if ev := p.b.expect(t, "motion"); ev.x != 7 || ev.y != 8 { | |||
| 437 | + | t.Fatalf("motion %+v", ev) | |||
| 438 | + | } | |||
| 439 | + | ||||
| 440 | + | // b accepts and prefers move: both sides learn the action. | |||
| 441 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |||
| 442 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove) | |||
| 443 | + | if ev := p.a.expect(t, "target"); ev.mime != "text/plain" { | |||
| 444 | + | t.Fatalf("target %+v", ev) | |||
| 445 | + | } | |||
| 446 | + | if ev := p.a.expect(t, "action"); ev.n != actMove { | |||
| 447 | + | t.Fatalf("action %+v", ev) | |||
| 448 | + | } | |||
| 449 | + | if ev := p.b.expect(t, "offer_action"); ev.n != actMove { | |||
| 450 | + | t.Fatalf("offer action %+v", ev) | |||
| 451 | + | } | |||
| 452 | + | ||||
| 453 | + | // Finishing before the drop is a protocol error, so only after it. | |||
| 454 | + | p.commands <- ports.DataDragDrop{} | |||
| 455 | + | p.b.expect(t, "drop") | |||
| 456 | + | p.b.expect(t, "leave") | |||
| 457 | + | p.a.expect(t, "drop_performed") | |||
| 458 | + | ||||
| 459 | + | var fds [2]int | |||
| 460 | + | if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil { | |||
| 461 | + | t.Fatal(err) | |||
| 462 | + | } | |||
| 463 | + | defer unix.Close(fds[0]) | |||
| 464 | + | if err := wireRequest(p.b.c, offer, uint16(wayland.DataOfferRequestReceive), []int{fds[1]}, "text/plain"); err != nil { | |||
| 465 | + | t.Fatal(err) | |||
| 466 | + | } | |||
| 467 | + | _ = unix.Close(fds[1]) | |||
| 468 | + | if err := p.b.c.Roundtrip(); err != nil { | |||
| 469 | + | t.Fatal(err) | |||
| 470 | + | } | |||
| 471 | + | if ev := p.a.expect(t, "send"); ev.mime != "text/plain" { | |||
| 472 | + | t.Fatalf("send %+v", ev) | |||
| 473 | + | } | |||
| 474 | + | buf := make([]byte, 16) | |||
| 475 | + | n, err := unix.Read(fds[0], buf) | |||
| 476 | + | for errors.Is(err, unix.EINTR) { | |||
| 477 | + | n, err = unix.Read(fds[0], buf) | |||
| 478 | + | } | |||
| 479 | + | if err != nil || string(buf[:n]) != "dnd data" { | |||
| 480 | + | t.Fatalf("received %q, %v", buf[:max(n, 0)], err) | |||
| 481 | + | } | |||
| 482 | + | ||||
| 483 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish) | |||
| 484 | + | p.a.expect(t, "finished") | |||
| 485 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy) | |||
| 486 | + | p.a.none(t) | |||
| 487 | + | p.b.none(t) | |||
| 488 | + | // core ended the drag: wayland does not echo it. | |||
| 489 | + | dndNoEvent[ports.DataDragEnded](t, p.events) | |||
| 490 | + | } | |||
| 491 | + | ||||
| 492 | + | // The action follows wlroots: the preferred one if both allow it, else the | |||
| 493 | + | // first of copy, move, ask they share, else none. | |||
| 494 | + | func TestChooseAction(t *testing.T) { | |||
| 495 | + | for _, tc := range []struct{ avail, preferred, want uint32 }{ | |||
| 496 | + | {actCopy | actMove, actMove, actMove}, | |||
| 497 | + | {actCopy | actMove, actAsk, actCopy}, | |||
| 498 | + | {actMove | actAsk, 0, actMove}, | |||
| 499 | + | {actAsk, 0, actAsk}, | |||
| 500 | + | {0, actCopy, 0}, | |||
| 501 | + | } { | |||
| 502 | + | if got := chooseAction(tc.avail, tc.preferred); got != tc.want { | |||
| 503 | + | t.Errorf("chooseAction(%d, %d) = %d, want %d", tc.avail, tc.preferred, got, tc.want) | |||
| 504 | + | } | |||
| 505 | + | } | |||
| 506 | + | } | |||
| 507 | + | ||||
| 508 | + | // A target and a source sharing no action leave the drop refused. | |||
| 509 | + | func TestDataDragNoCommonAction(t *testing.T) { | |||
| 510 | + | p := newDndPair(t) | |||
| 511 | + | src := p.a.source(t, actCopy, "text/plain") | |||
| 512 | + | p.a.startDrag(t, src, 0, p.a.press(t, p.commands)) | |||
| 513 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |||
| 514 | + | p.b.expect(t, "offer") | |||
| 515 | + | p.b.expect(t, "source_actions") | |||
| 516 | + | offer := p.b.expect(t, "enter").n | |||
| 517 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |||
| 518 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actMove, actMove) | |||
| 519 | + | p.a.expect(t, "target") | |||
| 520 | + | p.commands <- ports.DataDragDrop{} | |||
| 521 | + | p.b.expect(t, "leave") | |||
| 522 | + | // The refused target no longer accepts. | |||
| 523 | + | if ev := p.a.expect(t, "target"); ev.mime != "" { | |||
| 524 | + | t.Fatalf("target %+v", ev) | |||
| 525 | + | } | |||
| 526 | + | p.a.expect(t, "cancelled") | |||
| 527 | + | } | |||
| 528 | + | ||||
| 529 | + | // A2: a release over nothing, or over a target that accepted nothing, | |||
| 530 | + | // cancels the source; the target gets leave and no drop. | |||
| 531 | + | func TestDataDragRefusedDrop(t *testing.T) { | |||
| 532 | + | t.Run("nothing", func(t *testing.T) { | |||
| 533 | + | p := newDndPair(t) | |||
| 534 | + | p.start(t, 0) | |||
| 535 | + | p.commands <- ports.DataDragDrop{} | |||
| 536 | + | p.a.expect(t, "cancelled") | |||
| 537 | + | p.b.none(t) | |||
| 538 | + | }) | |||
| 539 | + | t.Run("not accepted", func(t *testing.T) { | |||
| 540 | + | p := newDndPair(t) | |||
| 541 | + | p.start(t, 0) | |||
| 542 | + | p.enter(t) | |||
| 543 | + | p.commands <- ports.DataDragDrop{} | |||
| 544 | + | p.b.expect(t, "leave") | |||
| 545 | + | p.a.expect(t, "cancelled") | |||
| 546 | + | p.b.none(t) | |||
| 547 | + | }) | |||
| 548 | + | t.Run("escape", func(t *testing.T) { | |||
| 549 | + | p := newDndPair(t) | |||
| 550 | + | p.start(t, 0) | |||
| 551 | + | p.enter(t) | |||
| 552 | + | p.commands <- ports.DataDragCancel{} | |||
| 553 | + | p.b.expect(t, "leave") | |||
| 554 | + | p.a.expect(t, "cancelled") | |||
| 555 | + | dndNoEvent[ports.DataDragEnded](t, p.events) | |||
| 556 | + | }) | |||
| 557 | + | } | |||
| 558 | + | ||||
| 559 | + | // Moving to another surface leaves the first and enters the second; a | |||
| 560 | + | // surface of nobody (ID 0) leaves. | |||
| 561 | + | func TestDataDragLeavesOnFocusChange(t *testing.T) { | |||
| 562 | + | p := newDndPair(t) | |||
| 563 | + | p.start(t, 0) | |||
| 564 | + | p.enter(t) | |||
| 565 | + | p.commands <- ports.DataDragFocus{} | |||
| 566 | + | p.b.expect(t, "leave") | |||
| 567 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 2, Y: 3} | |||
| 568 | + | for range 3 { | |||
| 569 | + | p.b.next(t) | |||
| 570 | + | } | |||
| 571 | + | if ev := p.b.expect(t, "enter"); ev.x != 2 || ev.y != 3 { | |||
| 572 | + | t.Fatalf("enter %+v", ev) | |||
| 573 | + | } | |||
| 574 | + | } | |||
| 575 | + | ||||
| 576 | + | // A4: the source dying mid-drag ends it: the target gets leave and core a | |||
| 577 | + | // DataDragEnded; later commands change nothing. | |||
| 578 | + | func TestDataDragSourceDestroyed(t *testing.T) { | |||
| 579 | + | p := newDndPair(t) | |||
| 580 | + | src := p.start(t, 0) | |||
| 581 | + | p.enter(t) | |||
| 582 | + | requestProtocol(t, p.a.c, src, wayland.DataSourceRequestDestroy) | |||
| 583 | + | if err := p.a.c.Roundtrip(); err != nil { | |||
| 584 | + | t.Fatal(err) | |||
| 585 | + | } | |||
| 586 | + | p.b.expect(t, "leave") | |||
| 587 | + | dndNextEvent[ports.DataDragEnded](t, p.events) | |||
| 588 | + | p.commands <- ports.DataDragMotion{ID: p.b.window, X: 9, Y: 9} | |||
| 589 | + | p.commands <- ports.DataDragDrop{} | |||
| 590 | + | p.b.none(t) | |||
| 591 | + | } | |||
| 592 | + | ||||
| 593 | + | // A drag whose origin client dies ends the same way. | |||
| 594 | + | func TestDataDragOriginDies(t *testing.T) { | |||
| 595 | + | p := newDndPair(t) | |||
| 596 | + | p.start(t, 0) | |||
| 597 | + | p.enter(t) | |||
| 598 | + | _ = p.a.c.Close() | |||
| 599 | + | p.b.expect(t, "leave") | |||
| 600 | + | dndNextEvent[ports.DataDragEnded](t, p.events) | |||
| 601 | + | } | |||
| 602 | + | ||||
| 603 | + | // A6: a start_drag without the current press of the origin client is | |||
| 604 | + | // refused: the source is cancelled and core hears nothing. | |||
| 605 | + | func TestDataDragInvalidStart(t *testing.T) { | |||
| 606 | + | t.Run("stale serial", func(t *testing.T) { | |||
| 607 | + | p := newDndPair(t) | |||
| 608 | + | serial := p.a.press(t, p.commands) | |||
| 609 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial-1) | |||
| 610 | + | p.a.expect(t, "cancelled") | |||
| 611 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |||
| 612 | + | }) | |||
| 613 | + | t.Run("other client's press", func(t *testing.T) { | |||
| 614 | + | p := newDndPair(t) | |||
| 615 | + | p.a.press(t, p.commands) | |||
| 616 | + | serial := p.b.press(t, p.commands) | |||
| 617 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |||
| 618 | + | p.a.expect(t, "cancelled") | |||
| 619 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |||
| 620 | + | }) | |||
| 621 | + | t.Run("key serial", func(t *testing.T) { | |||
| 622 | + | p := newDndPair(t) | |||
| 623 | + | p.a.press(t, p.commands) | |||
| 624 | + | p.a.release(t, p.commands) | |||
| 625 | + | serial := p.a.key(t, p.commands) | |||
| 626 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |||
| 627 | + | p.a.expect(t, "cancelled") | |||
| 628 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |||
| 629 | + | }) | |||
| 630 | + | t.Run("serial after the release", func(t *testing.T) { | |||
| 631 | + | p := newDndPair(t) | |||
| 632 | + | serial := p.a.press(t, p.commands) | |||
| 633 | + | p.a.release(t, p.commands) | |||
| 634 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |||
| 635 | + | p.a.expect(t, "cancelled") | |||
| 636 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |||
| 637 | + | }) | |||
| 638 | + | t.Run("other surface of the client", func(t *testing.T) { | |||
| 639 | + | p := newDndPair(t) | |||
| 640 | + | serial := p.a.press(t, p.commands) | |||
| 641 | + | other := p.a.surfaceNew(t) | |||
| 642 | + | p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), other, 0, serial) | |||
| 643 | + | p.a.expect(t, "cancelled") | |||
| 644 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |||
| 645 | + | }) | |||
| 646 | + | t.Run("press of the first button", func(t *testing.T) { | |||
| 647 | + | // A second button does not make a new grab: only the first serial. | |||
| 648 | + | p := newDndPair(t) | |||
| 649 | + | first := p.a.press(t, p.commands) | |||
| 650 | + | p.commands <- ports.PointerButtonTo{ID: p.a.window, Button: 0x111, Pressed: true, Time: 2 * time.Millisecond} | |||
| 651 | + | second := p.a.press2(t) | |||
| 652 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, second) | |||
| 653 | + | p.a.expect(t, "cancelled") | |||
| 654 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, first) | |||
| 655 | + | // Core is told the grab's button: its release drops. | |||
| 656 | + | if ev := dndNextEvent[ports.DataDragStarted](t, p.events); ev.Button != 0x110 { | |||
| 657 | + | t.Fatalf("drag button %#x, want the grab's", ev.Button) | |||
| 658 | + | } | |||
| 659 | + | }) | |||
| 660 | + | t.Run("grab release consumed by core", func(t *testing.T) { | |||
| 661 | + | // A window drag consumes the release: the next grab press | |||
| 662 | + | // still makes the serial a drag can name. | |||
| 663 | + | p := newDndPair(t) | |||
| 664 | + | stale := p.a.press(t, p.commands) | |||
| 665 | + | serial := p.a.press(t, p.commands) | |||
| 666 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, stale) | |||
| 667 | + | p.a.expect(t, "cancelled") | |||
| 668 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |||
| 669 | + | dndNextEvent[ports.DataDragStarted](t, p.events) | |||
| 670 | + | }) | |||
| 671 | + | t.Run("grab release of a gone window", func(t *testing.T) { | |||
| 672 | + | p := newDndPair(t) | |||
| 673 | + | serial := p.a.press(t, p.commands) | |||
| 674 | + | p.commands <- ports.PointerButtonTo{ID: 9999, Button: 0x110, Time: 2 * time.Millisecond} | |||
| 675 | + | p.a.key(t, p.commands) // the release is applied once the key is seen | |||
| 676 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial) | |||
| 677 | + | p.a.expect(t, "cancelled") | |||
| 678 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |||
| 679 | + | }) | |||
| 680 | + | t.Run("used source", func(t *testing.T) { | |||
| 681 | + | p := newDndPair(t) | |||
| 682 | + | src := p.start(t, 0) | |||
| 683 | + | p.commands <- ports.DataDragCancel{} | |||
| 684 | + | p.a.expect(t, "cancelled") | |||
| 685 | + | p.a.startDrag(t, src, 0, p.a.press(t, p.commands)) | |||
| 686 | + | p.a.expect(t, "cancelled") | |||
| 687 | + | }) | |||
| 688 | + | t.Run("drag in progress", func(t *testing.T) { | |||
| 689 | + | p := newDndPair(t) | |||
| 690 | + | p.start(t, 0) | |||
| 691 | + | dndNextEvent[ports.DataDragStarted](t, p.events) | |||
| 692 | + | other := p.a.source(t, actCopy, "text/plain") | |||
| 693 | + | p.a.startDrag(t, other, 0, p.a.press(t, p.commands)) | |||
| 694 | + | p.a.expect(t, "cancelled") | |||
| 695 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |||
| 696 | + | }) | |||
| 697 | + | t.Run("icon with another role", func(t *testing.T) { | |||
| 698 | + | p := newDndPair(t) | |||
| 699 | + | serial := p.a.press(t, p.commands) | |||
| 700 | + | src := p.a.source(t, actCopy, "text/plain") | |||
| 701 | + | // The origin toplevel is a role already. | |||
| 702 | + | requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestStartDrag, src, p.a.surf, p.a.surf, serial) | |||
| 703 | + | expectProtocolError(t, p.a.c, p.a.device.ID(), uint32(wayland.DataDeviceErrorRole)) | |||
| 704 | + | dndNoEvent[ports.DataDragStarted](t, p.events) | |||
| 705 | + | }) | |||
| 706 | + | } | |||
| 707 | + | ||||
| 708 | + | // A nil source is a drag internal to the origin client: only its own | |||
| 709 | + | // surfaces see it, without offer. | |||
| 710 | + | func TestDataDragWithoutSource(t *testing.T) { | |||
| 711 | + | p := newDndPair(t) | |||
| 712 | + | p.a.startDrag(t, 0, 0, p.a.press(t, p.commands)) | |||
| 713 | + | dndNextEvent[ports.DataDragStarted](t, p.events) | |||
| 714 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |||
| 715 | + | p.b.none(t) | |||
| 716 | + | p.commands <- ports.DataDragFocus{ID: p.a.window, X: 4, Y: 5} | |||
| 717 | + | if ev := p.a.expect(t, "enter"); ev.n != 0 || ev.x != 4 || ev.y != 5 { | |||
| 718 | + | t.Fatalf("enter %+v", ev) | |||
| 719 | + | } | |||
| 720 | + | p.commands <- ports.DataDragDrop{} | |||
| 721 | + | p.a.expect(t, "drop") | |||
| 722 | + | } | |||
| 723 | + | ||||
| 724 | + | // Offer and source requests are validated. | |||
| 725 | + | func TestDataDragProtocolErrors(t *testing.T) { | |||
| 726 | + | t.Run("finish before drop", func(t *testing.T) { | |||
| 727 | + | p := newDndPair(t) | |||
| 728 | + | p.start(t, 0) | |||
| 729 | + | offer := p.enter(t) | |||
| 730 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish) | |||
| 731 | + | expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidFinish)) | |||
| 732 | + | }) | |||
| 733 | + | t.Run("offer action mask", func(t *testing.T) { | |||
| 734 | + | p := newDndPair(t) | |||
| 735 | + | p.start(t, 0) | |||
| 736 | + | offer := p.enter(t) | |||
| 737 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, uint32(8), uint32(0)) | |||
| 738 | + | expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidActionMask)) | |||
| 739 | + | }) | |||
| 740 | + | t.Run("offer preferred action", func(t *testing.T) { | |||
| 741 | + | p := newDndPair(t) | |||
| 742 | + | p.start(t, 0) | |||
| 743 | + | offer := p.enter(t) | |||
| 744 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actMove) | |||
| 745 | + | expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidAction)) | |||
| 746 | + | }) | |||
| 747 | + | t.Run("source action mask", func(t *testing.T) { | |||
| 748 | + | p := newDndPair(t) | |||
| 749 | + | src := p.a.source(t, 0, "text/plain") | |||
| 750 | + | requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, uint32(16)) | |||
| 751 | + | expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidActionMask)) | |||
| 752 | + | }) | |||
| 753 | + | t.Run("selection with actions", func(t *testing.T) { | |||
| 754 | + | p := newDndPair(t) | |||
| 755 | + | src := p.a.source(t, actCopy, "text/plain") | |||
| 756 | + | requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestSetSelection, src, uint32(0)) | |||
| 757 | + | expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource)) | |||
| 758 | + | }) | |||
| 759 | + | t.Run("actions after the drag started", func(t *testing.T) { | |||
| 760 | + | p := newDndPair(t) | |||
| 761 | + | src := p.start(t, 0) | |||
| 762 | + | requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, actCopy) | |||
| 763 | + | expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource)) | |||
| 764 | + | }) | |||
| 765 | + | } | |||
| 766 | + | ||||
| 767 | + | // A destroyed offer before finish cancels the source. | |||
| 768 | + | func TestDataDragOfferDestroyedBeforeFinish(t *testing.T) { | |||
| 769 | + | p := newDndPair(t) | |||
| 770 | + | p.start(t, 0) | |||
| 771 | + | offer := p.enter(t) | |||
| 772 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |||
| 773 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy) | |||
| 774 | + | p.a.expect(t, "target") | |||
| 775 | + | p.a.expect(t, "action") | |||
| 776 | + | p.commands <- ports.DataDragDrop{} | |||
| 777 | + | p.b.expect(t, "offer_action") | |||
| 778 | + | p.b.expect(t, "drop") | |||
| 779 | + | p.b.expect(t, "leave") | |||
| 780 | + | p.a.expect(t, "drop_performed") | |||
| 781 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy) | |||
| 782 | + | p.a.expect(t, "cancelled") | |||
| 783 | + | } | |||
| 784 | + | ||||
| 785 | + | // iconContent reports whether the server still holds content for id. | |||
| 786 | + | func iconContent(t *testing.T, s *Server, id ports.WindowID) bool { | |||
| 787 | + | t.Helper() | |||
| 788 | + | var ok bool | |||
| 789 | + | if !s.display.Do(func() { _, ok = s.latest[id] }) { | |||
| 790 | + | t.Fatal("display stopped") | |||
| 791 | + | } | |||
| 792 | + | return ok | |||
| 793 | + | } | |||
| 794 | + | ||||
| 795 | + | // The icon is a surface with its own window ID: its size and the | |||
| 796 | + | // offsets of its commits reach core, its content is published, and both go | |||
| 797 | + | // when the drag ends. | |||
| 798 | + | func TestDataDragIcon(t *testing.T) { | |||
| 799 | + | p := newDndPair(t) | |||
| 800 | + | icon := p.a.surfaceNew(t) | |||
| 801 | + | buf := p.a.buffer(t, 8, 6) | |||
| 802 | + | // An offset on an ordinary surface is not used. | |||
| 803 | + | requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestOffset, int32(5), int32(5)) | |||
| 804 | + | requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestCommit) | |||
| 805 | + | ||||
| 806 | + | src := p.a.source(t, actCopy, "text/plain") | |||
| 807 | + | p.a.startDrag(t, src, icon, p.a.press(t, p.commands)) | |||
| 808 | + | started := dndNextEvent[ports.DataDragStarted](t, p.events) | |||
| 809 | + | if started.Icon == 0 { | |||
| 810 | + | t.Fatal("no icon ID") | |||
| 811 | + | } | |||
| 812 | + | id := started.Icon | |||
| 813 | + | if iconContent(t, p.s, id) { | |||
| 814 | + | t.Fatal("content before the first commit") | |||
| 815 | + | } | |||
| 816 | + | ||||
| 817 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 818 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-3), int32(-4)) | |||
| 819 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |||
| 820 | + | if err := p.a.c.Roundtrip(); err != nil { | |||
| 821 | + | t.Fatal(err) | |||
| 822 | + | } | |||
| 823 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -3, Y: -4, W: 8, H: 6}) { | |||
| 824 | + | t.Fatalf("icon %+v", got) | |||
| 825 | + | } | |||
| 826 | + | if !iconContent(t, p.s, id) { | |||
| 827 | + | t.Fatal("no icon content") | |||
| 828 | + | } | |||
| 829 | + | ||||
| 830 | + | // Offsets add up from commit to commit. | |||
| 831 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(2)) | |||
| 832 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |||
| 833 | + | if err := p.a.c.Roundtrip(); err != nil { | |||
| 834 | + | t.Fatal(err) | |||
| 835 | + | } | |||
| 836 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -4, Y: -2, W: 8, H: 6}) { | |||
| 837 | + | t.Fatalf("icon %+v", got) | |||
| 838 | + | } | |||
| 839 | + | // The icon never takes input. | |||
| 840 | + | dndNoEvent[ports.InputRegionChanged](t, p.events) | |||
| 841 | + | ||||
| 842 | + | p.commands <- ports.DataDragCancel{} | |||
| 843 | + | p.a.expect(t, "cancelled") | |||
| 844 | + | if iconContent(t, p.s, id) { | |||
| 845 | + | t.Fatal("icon content kept after the drag") | |||
| 846 | + | } | |||
| 847 | + | } | |||
| 848 | + | ||||
| 849 | + | // An icon committed before start_drag shows at once. | |||
| 850 | + | func TestDataDragIconCommittedEarly(t *testing.T) { | |||
| 851 | + | p := newDndPair(t) | |||
| 852 | + | icon := p.a.surfaceNew(t) | |||
| 853 | + | buf := p.a.buffer(t, 4, 4) | |||
| 854 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 855 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |||
| 856 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands)) | |||
| 857 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |||
| 858 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, W: 4, H: 4}) { | |||
| 859 | + | t.Fatalf("icon %+v", got) | |||
| 860 | + | } | |||
| 861 | + | if !iconContent(t, p.s, id) { | |||
| 862 | + | t.Fatal("no icon content") | |||
| 863 | + | } | |||
| 864 | + | } | |||
| 865 | + | ||||
| 866 | + | // Before wl_surface v5 the icon offset rides on attach and adds up. | |||
| 867 | + | func TestDataDragIconAttachOffset(t *testing.T) { | |||
| 868 | + | p := newDndPair(t) | |||
| 869 | + | comp4 := bindVersion(t, p.a.c, "wl_compositor", 4) | |||
| 870 | + | icon := p.a.c.AllocateID() | |||
| 871 | + | requestProtocol(t, p.a.c, comp4, wayland.CompositorRequestCreateSurface, icon) | |||
| 872 | + | registerProtocol(t, p.a.c, icon) | |||
| 873 | + | buf := p.a.buffer(t, 4, 4) | |||
| 874 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands)) | |||
| 875 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |||
| 876 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(-2), int32(-1)) | |||
| 877 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |||
| 878 | + | if err := p.a.c.Roundtrip(); err != nil { | |||
| 879 | + | t.Fatal(err) | |||
| 880 | + | } | |||
| 881 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -2, Y: -1, W: 4, H: 4}) { | |||
| 882 | + | t.Fatalf("icon %+v", got) | |||
| 883 | + | } | |||
| 884 | + | } | |||
| 885 | + | ||||
| 886 | + | // Destroying the icon mid-drag hides it; the drag goes on. | |||
| 887 | + | func TestDataDragIconDestroyed(t *testing.T) { | |||
| 888 | + | p := newDndPair(t) | |||
| 889 | + | icon := p.a.surfaceNew(t) | |||
| 890 | + | buf := p.a.buffer(t, 8, 6) | |||
| 891 | + | p.a.startDrag(t, p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list"), icon, p.a.press(t, p.commands)) | |||
| 892 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |||
| 893 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 894 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |||
| 895 | + | if err := p.a.c.Roundtrip(); err != nil { | |||
| 896 | + | t.Fatal(err) | |||
| 897 | + | } | |||
| 898 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got.W != 8 { | |||
| 899 | + | t.Fatalf("icon %+v", got) | |||
| 900 | + | } | |||
| 901 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestDestroy) | |||
| 902 | + | if err := p.a.c.Roundtrip(); err != nil { | |||
| 903 | + | t.Fatal(err) | |||
| 904 | + | } | |||
| 905 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id}) { | |||
| 906 | + | t.Fatalf("icon %+v", got) | |||
| 907 | + | } | |||
| 908 | + | if iconContent(t, p.s, id) { | |||
| 909 | + | t.Fatal("icon content kept") | |||
| 910 | + | } | |||
| 911 | + | p.enter(t) | |||
| 912 | + | } | |||
| 913 | + | ||||
| 914 | + | // lockedPair is a pair of clients on a server that can lock the session. | |||
| 915 | + | func lockedPair(t *testing.T) (*dndPair, string) { | |||
| 916 | + | t.Helper() | |||
| 917 | + | gate := &sessionsecurity.Gate{} | |||
| 918 | + | events := make(chan ports.ClientEvent, 128) | |||
| 919 | + | commands := make(chan ports.ClientCommand, 32) | |||
| 920 | + | dir := t.TempDir() | |||
| 921 | + | 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")) | |||
| 922 | + | if err != nil { | |||
| 923 | + | t.Fatal(err) | |||
| 924 | + | } | |||
| 925 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 926 | + | done := make(chan error, 1) | |||
| 927 | + | go func() { done <- s.Run(ctx) }() | |||
| 928 | + | t.Cleanup(func() { | |||
| 929 | + | cancel() | |||
| 930 | + | select { | |||
| 931 | + | case err := <-done: | |||
| 932 | + | if err != nil { | |||
| 933 | + | t.Error(err) | |||
| 934 | + | } | |||
| 935 | + | case <-time.After(3 * time.Second): | |||
| 936 | + | t.Error("server did not stop") | |||
| 937 | + | } | |||
| 938 | + | }) | |||
| 939 | + | p := &dndPair{s: s, events: events, commands: commands} | |||
| 940 | + | p.a = newDndClient(t, s, dir, events) | |||
| 941 | + | p.b = newDndClient(t, s, dir, events) | |||
| 942 | + | return p, dir | |||
| 943 | + | } | |||
| 944 | + | ||||
| 945 | + | func lockSession(t *testing.T, s *Server, dir string) { | |||
| 946 | + | t.Helper() | |||
| 947 | + | lc := protocolClient(t, s, dir) | |||
| 948 | + | if _, err := bindLockManager(t, lc).Lock(); err != nil { | |||
| 949 | + | t.Fatal(err) | |||
| 950 | + | } | |||
| 951 | + | if err := lc.Roundtrip(); err != nil { | |||
| 952 | + | t.Fatal(err) | |||
| 953 | + | } | |||
| 954 | + | } | |||
| 955 | + | ||||
| 956 | + | // A7: the session lock cancels a drag at once: target leave, source | |||
| 957 | + | // cancelled, icon gone. | |||
| 958 | + | func TestDataDragLockCancels(t *testing.T) { | |||
| 959 | + | p, dir := lockedPair(t) | |||
| 960 | + | icon := p.a.surfaceNew(t) | |||
| 961 | + | buf := p.a.buffer(t, 8, 6) | |||
| 962 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 963 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |||
| 964 | + | src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list") | |||
| 965 | + | p.a.startDrag(t, src, icon, p.a.press(t, p.commands)) | |||
| 966 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |||
| 967 | + | p.enter(t) | |||
| 968 | + | ||||
| 969 | + | lockSession(t, p.s, dir) | |||
| 970 | + | p.b.expect(t, "leave") | |||
| 971 | + | p.a.expect(t, "cancelled") | |||
| 972 | + | if iconContent(t, p.s, id) { | |||
| 973 | + | t.Fatal("icon content kept") | |||
| 974 | + | } | |||
| 975 | + | // A new drag is refused while protected. | |||
| 976 | + | src2 := p.a.source(t, actCopy, "text/plain") | |||
| 977 | + | p.a.startDrag(t, src2, 0, uint32(1)) | |||
| 978 | + | p.a.expect(t, "cancelled") | |||
| 979 | + | } | |||
| 980 | + | ||||
| 981 | + | // GO-006: a dropped transfer waiting for its finish dies with the lock, for | |||
| 982 | + | // a v3 and a pre-v3 target alike. | |||
| 983 | + | func TestDataDropLockCancelsPending(t *testing.T) { | |||
| 984 | + | for _, version := range []uint32{3, 2} { | |||
| 985 | + | p, dir := lockedPair(t) | |||
| 986 | + | p.b = newDndClientAt(t, p.s, dir, p.events, version) | |||
| 987 | + | p.start(t, 0) | |||
| 988 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6} | |||
| 989 | + | p.b.until(t, "enter") | |||
| 990 | + | offer := p.b.lastOffer(t) | |||
| 991 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |||
| 992 | + | if version >= 3 { | |||
| 993 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy) | |||
| 994 | + | } | |||
| 995 | + | p.a.until(t, "target") | |||
| 996 | + | p.commands <- ports.DataDragDrop{} | |||
| 997 | + | p.a.until(t, "drop_performed") | |||
| 998 | + | lockSession(t, p.s, dir) | |||
| 999 | + | p.a.expect(t, "cancelled") | |||
| 1000 | + | } | |||
| 1001 | + | } | |||
| 1002 | + | ||||
| 1003 | + | // A subsurface of the focused tree is a valid origin. | |||
| 1004 | + | func TestDataDragFromSubsurface(t *testing.T) { | |||
| 1005 | + | p := newDndPair(t) | |||
| 1006 | + | serial := p.a.press(t, p.commands) | |||
| 1007 | + | child := p.a.surfaceNew(t) | |||
| 1008 | + | subc := bindProtocol(t, p.a.c, "wl_subcompositor") | |||
| 1009 | + | requestProtocol(t, p.a.c, subc, wayland.SubcompositorRequestGetSubsurface, p.a.c.AllocateID(), child, p.a.surf) | |||
| 1010 | + | p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), child, 0, serial) | |||
| 1011 | + | dndNextEvent[ports.DataDragStarted](t, p.events) | |||
| 1012 | + | } | |||
| 1013 | + | ||||
| 1014 | + | // GO-003: destroying the offers of the target withdraws the acceptance: the | |||
| 1015 | + | // release then cancels, with no drop. | |||
| 1016 | + | func TestDataDragOfferDestroyedBeforeRelease(t *testing.T) { | |||
| 1017 | + | p := newDndPair(t) | |||
| 1018 | + | p.start(t, 0) | |||
| 1019 | + | offer := p.enter(t) | |||
| 1020 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |||
| 1021 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy) | |||
| 1022 | + | p.a.expect(t, "target") | |||
| 1023 | + | p.a.expect(t, "action") | |||
| 1024 | + | requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy) | |||
| 1025 | + | if err := p.b.c.Roundtrip(); err != nil { | |||
| 1026 | + | t.Fatal(err) | |||
| 1027 | + | } | |||
| 1028 | + | if ev := p.a.expect(t, "target"); ev.mime != "" { | |||
| 1029 | + | t.Fatalf("target %+v", ev) | |||
| 1030 | + | } | |||
| 1031 | + | if ev := p.a.expect(t, "action"); ev.n != 0 { | |||
| 1032 | + | t.Fatalf("action %+v", ev) | |||
| 1033 | + | } | |||
| 1034 | + | p.commands <- ports.DataDragDrop{} | |||
| 1035 | + | got := p.b.until(t, "leave") | |||
| 1036 | + | for _, ev := range got { | |||
| 1037 | + | if ev.name == "drop" { | |||
| 1038 | + | t.Fatalf("drop after the offer was destroyed: %+v", got) | |||
| 1039 | + | } | |||
| 1040 | + | } | |||
| 1041 | + | p.a.expect(t, "cancelled") | |||
| 1042 | + | } | |||
| 1043 | + | ||||
| 1044 | + | // GO-004: with ask, the target picks the final action after the drop; the | |||
| 1045 | + | // source hears it just before dnd_finished. | |||
| 1046 | + | func TestDataDragAsk(t *testing.T) { | |||
| 1047 | + | p := newDndPair(t) | |||
| 1048 | + | src := p.a.source(t, actCopy|actMove|actAsk, "text/plain") | |||
| 1049 | + | p.a.startDrag(t, src, 0, p.a.press(t, p.commands)) | |||
| 1050 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |||
| 1051 | + | p.b.until(t, "enter") | |||
| 1052 | + | id := p.b.lastOffer(t) | |||
| 1053 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |||
| 1054 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk) | |||
| 1055 | + | p.a.expect(t, "target") | |||
| 1056 | + | if ev := p.a.expect(t, "action"); ev.n != actAsk { | |||
| 1057 | + | t.Fatalf("action %+v", ev) | |||
| 1058 | + | } | |||
| 1059 | + | p.commands <- ports.DataDragDrop{} | |||
| 1060 | + | p.b.until(t, "leave") | |||
| 1061 | + | p.a.expect(t, "drop_performed") | |||
| 1062 | + | ||||
| 1063 | + | // Finishing while the action is ask is an error; the choice comes first. | |||
| 1064 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(2), "text/plain") | |||
| 1065 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove) | |||
| 1066 | + | p.a.expect(t, "target") | |||
| 1067 | + | p.b.none(t) // no offer.action after the drop | |||
| 1068 | + | p.a.none(t) // the final action waits for the finish | |||
| 1069 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestFinish) | |||
| 1070 | + | if ev := p.a.expect(t, "action"); ev.n != actMove { | |||
| 1071 | + | t.Fatalf("action %+v", ev) | |||
| 1072 | + | } | |||
| 1073 | + | p.a.expect(t, "finished") | |||
| 1074 | + | } | |||
| 1075 | + | ||||
| 1076 | + | // A final preferred action outside the source's is an error. | |||
| 1077 | + | func TestDataDragAskFinalActionNotInSource(t *testing.T) { | |||
| 1078 | + | p := newDndPair(t) | |||
| 1079 | + | src := p.a.source(t, actCopy|actAsk, "text/plain") | |||
| 1080 | + | p.a.startDrag(t, src, 0, p.a.press(t, p.commands)) | |||
| 1081 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |||
| 1082 | + | p.b.until(t, "enter") | |||
| 1083 | + | id := p.b.lastOffer(t) | |||
| 1084 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |||
| 1085 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk) | |||
| 1086 | + | p.a.expect(t, "target") | |||
| 1087 | + | p.a.expect(t, "action") | |||
| 1088 | + | p.commands <- ports.DataDragDrop{} | |||
| 1089 | + | p.b.until(t, "leave") | |||
| 1090 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actMove, actMove) | |||
| 1091 | + | expectProtocolError(t, p.b.c, id, uint32(wayland.DataOfferErrorInvalidAction)) | |||
| 1092 | + | } | |||
| 1093 | + | ||||
| 1094 | + | // GO-005: a pre-v3 target copies: a source that does not allow copy is | |||
| 1095 | + | // cancelled, one that does gets it. | |||
| 1096 | + | func TestDataDragLegacyTarget(t *testing.T) { | |||
| 1097 | + | setup := func(t *testing.T, actions uint32) (*dndPair, uint32) { | |||
| 1098 | + | s, events, commands, dir := keyboardServer(t) | |||
| 1099 | + | p := &dndPair{s: s, events: events, commands: commands} | |||
| 1100 | + | p.a = newDndClient(t, s, dir, events) | |||
| 1101 | + | p.b = newDndClientAt(t, s, dir, events, 2) | |||
| 1102 | + | src := p.a.source(t, actions, "text/plain") | |||
| 1103 | + | p.a.startDrag(t, src, 0, p.a.press(t, p.commands)) | |||
| 1104 | + | p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1} | |||
| 1105 | + | p.b.until(t, "enter") | |||
| 1106 | + | id := p.b.lastOffer(t) | |||
| 1107 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain") | |||
| 1108 | + | p.a.expect(t, "target") | |||
| 1109 | + | return p, id | |||
| 1110 | + | } | |||
| 1111 | + | t.Run("move only", func(t *testing.T) { | |||
| 1112 | + | p, _ := setup(t, actMove) | |||
| 1113 | + | p.commands <- ports.DataDragDrop{} | |||
| 1114 | + | got := p.b.until(t, "leave") | |||
| 1115 | + | for _, ev := range got { | |||
| 1116 | + | if ev.name == "drop" { | |||
| 1117 | + | t.Fatalf("drop for a source without copy: %+v", got) | |||
| 1118 | + | } | |||
| 1119 | + | } | |||
| 1120 | + | p.a.until(t, "cancelled") | |||
| 1121 | + | }) | |||
| 1122 | + | t.Run("copy", func(t *testing.T) { | |||
| 1123 | + | p, id := setup(t, actCopy|actMove) | |||
| 1124 | + | p.commands <- ports.DataDragDrop{} | |||
| 1125 | + | p.b.expect(t, "drop") | |||
| 1126 | + | p.b.expect(t, "leave") | |||
| 1127 | + | if ev := p.a.expect(t, "action"); ev.n != actCopy { | |||
| 1128 | + | t.Fatalf("action %+v", ev) | |||
| 1129 | + | } | |||
| 1130 | + | p.a.expect(t, "drop_performed") | |||
| 1131 | + | // GO-006: destroying the last offer finishes the transfer. | |||
| 1132 | + | requestProtocol(t, p.b.c, id, wayland.DataOfferRequestDestroy) | |||
| 1133 | + | p.a.expect(t, "finished") | |||
| 1134 | + | }) | |||
| 1135 | + | } | |||
| 1136 | + | ||||
| 1137 | + | // GO-007: of several offset requests before one commit, the last counts. | |||
| 1138 | + | func TestDataDragIconOffsetReplacedBeforeCommit(t *testing.T) { | |||
| 1139 | + | p := newDndPair(t) | |||
| 1140 | + | icon := p.a.surfaceNew(t) | |||
| 1141 | + | buf := p.a.buffer(t, 8, 6) | |||
| 1142 | + | p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands)) | |||
| 1143 | + | id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon | |||
| 1144 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 1145 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(-1)) | |||
| 1146 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-5), int32(-7)) | |||
| 1147 | + | requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit) | |||
| 1148 | + | if err := p.a.c.Roundtrip(); err != nil { | |||
| 1149 | + | t.Fatal(err) | |||
| 1150 | + | } | |||
| 1151 | + | if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -5, Y: -7, W: 8, H: 6}) { | |||
| 1152 | + | t.Fatalf("icon %+v", got) | |||
| 1153 | + | } | |||
| 1154 | + | } | |||
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/seat.go +15 -0
| @@ -43,6 +43,13 @@ type seatState struct { | |||
| 43 | 43 | // pressClient: popup grabs must come from it. | |
| 44 | 44 | press uint32 | |
| 45 | 45 | pressClient server.Client | |
| 46 | + | // grabSerial is the serial of the press that started the implicit | |
| 47 | + | // grab (core marks it), grabButton its button: the only serial | |
| 48 | + | // start_drag accepts (wlr_seat_validate_pointer_grab_serial). The | |
| 49 | + | // grab's release clears it; core may consume that release (a window | |
| 50 | + | // drag), and then the next grab press replaces it. | |
| 51 | + | grabSerial uint32 | |
| 52 | + | grabButton uint32 | |
| 46 | 53 | // pressAt and focusAt date the last press and keyboard focus change, | |
| 47 | 54 | // for xdg-activation tokens. | |
| 48 | 55 | pressAt, focusAt time.Time | |
| @@ -127,12 +134,20 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||
| 127 | 134 | } | |
| 128 | 135 | case ports.PointerButtonTo: | |
| 129 | 136 | // Releases may target the implicit-grab window after focus has moved. | |
| 137 | + | if c.Pressed && c.Grab || !c.Pressed && c.Button == s.seat.grabButton { | |
| 138 | + | // A new grab, or the end of this one even when its window is | |
| 139 | + | // gone. | |
| 140 | + | s.seat.grabSerial, s.seat.grabButton = 0, 0 | |
| 141 | + | } | |
| 130 | 142 | if client, pointers, ok := s.pointerTarget(c.ID); ok { | |
| 131 | 143 | s.serial++ | |
| 132 | 144 | state := uint32(0) | |
| 133 | 145 | if c.Pressed { | |
| 134 | 146 | state = 1 | |
| 135 | 147 | s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now() | |
| 148 | + | if c.Grab { | |
| 149 | + | s.seat.grabSerial, s.seat.grabButton = s.serial, c.Button | |
| 150 | + | } | |
| 136 | 151 | } | |
| 137 | 152 | for _, p := range pointers { | |
| 138 | 153 | s.stampPointer(p, c.Time) | |
| @@ -43,6 +43,13 @@ type seatState struct { | |||||
| 43 | // pressClient: popup grabs must come from it. | 43 | // pressClient: popup grabs must come from it. | ||
| 44 | press uint32 | 44 | press uint32 | ||
| 45 | pressClient server.Client | 45 | pressClient server.Client | ||
| 46 | + | // grabSerial is the serial of the press that started the implicit | |||
| 47 | + | // grab (core marks it), grabButton its button: the only serial | |||
| 48 | + | // start_drag accepts (wlr_seat_validate_pointer_grab_serial). The | |||
| 49 | + | // grab's release clears it; core may consume that release (a window | |||
| 50 | + | // drag), and then the next grab press replaces it. | |||
| 51 | + | grabSerial uint32 | |||
| 52 | + | grabButton uint32 | |||
| 46 | // pressAt and focusAt date the last press and keyboard focus change, | 53 | // pressAt and focusAt date the last press and keyboard focus change, | ||
| 47 | // for xdg-activation tokens. | 54 | // for xdg-activation tokens. | ||
| 48 | pressAt, focusAt time.Time | 55 | pressAt, focusAt time.Time | ||
| @@ -127,12 +134,20 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||||
| 127 | } | 134 | } | ||
| 128 | case ports.PointerButtonTo: | 135 | case ports.PointerButtonTo: | ||
| 129 | // Releases may target the implicit-grab window after focus has moved. | 136 | // Releases may target the implicit-grab window after focus has moved. | ||
| 137 | + | if c.Pressed && c.Grab || !c.Pressed && c.Button == s.seat.grabButton { | |||
| 138 | + | // A new grab, or the end of this one even when its window is | |||
| 139 | + | // gone. | |||
| 140 | + | s.seat.grabSerial, s.seat.grabButton = 0, 0 | |||
| 141 | + | } | |||
| 130 | if client, pointers, ok := s.pointerTarget(c.ID); ok { | 142 | if client, pointers, ok := s.pointerTarget(c.ID); ok { | ||
| 131 | s.serial++ | 143 | s.serial++ | ||
| 132 | state := uint32(0) | 144 | state := uint32(0) | ||
| 133 | if c.Pressed { | 145 | if c.Pressed { | ||
| 134 | state = 1 | 146 | state = 1 | ||
| 135 | s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now() | 147 | s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now() | ||
| 148 | + | if c.Grab { | |||
| 149 | + | s.seat.grabSerial, s.seat.grabButton = s.serial, c.Button | |||
| 150 | + | } | |||
| 136 | } | 151 | } | ||
| 137 | for _, p := range pointers { | 152 | for _, p := range pointers { | ||
| 138 | s.stampPointer(p, c.Time) | 153 | s.stampPointer(p, c.Time) | ||
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 +2 -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() | |
| @@ -146,6 +147,7 @@ func (s *Server) resetProtectionInput() { | |||
| 146 | 147 | clear(s.seat.grabKeys) | |
| 147 | 148 | s.seat.modState = ports.ModState{} | |
| 148 | 149 | s.seat.press, s.seat.pressClient = 0, server.Client{} | |
| 150 | + | s.seat.grabSerial, s.seat.grabButton = 0, 0 | |
| 149 | 151 | s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{} | |
| 150 | 152 | s.cursorSurface = nil | |
| 151 | 153 | } | |
| @@ -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() | ||
| @@ -146,6 +147,7 @@ func (s *Server) resetProtectionInput() { | |||||
| 146 | clear(s.seat.grabKeys) | 147 | clear(s.seat.grabKeys) | ||
| 147 | s.seat.modState = ports.ModState{} | 148 | s.seat.modState = ports.ModState{} | ||
| 148 | s.seat.press, s.seat.pressClient = 0, server.Client{} | 149 | s.seat.press, s.seat.pressClient = 0, server.Client{} | ||
| 150 | + | s.seat.grabSerial, s.seat.grabButton = 0, 0 | |||
| 149 | s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{} | 151 | s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{} | ||
| 150 | s.cursorSurface = nil | 152 | s.cursorSurface = nil | ||
| 151 | } | 153 | } | ||
M internal/adapters/wayland/surface.go +26 -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,10 @@ 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 are the last attach offset or wl_surface.offset request since | |
| 143 | + | // the commit (the latest replaces the earlier); only a drag icon uses | |
| 144 | + | // them. | |
| 145 | + | dx, dy int32 | |
| 140 | 146 | // damage is in surface (logical) pixels, bufDamage in buffer pixels. | |
| 141 | 147 | damage, bufDamage []ports.Rect | |
| 142 | 148 | // feedback are wp_presentation feedbacks for this commit. | |
| @@ -192,6 +198,9 @@ func (s *surface) windowID() ports.WindowID { | |||
| 192 | 198 | if s.lock != nil && s.lock.mapped { | |
| 193 | 199 | return s.lock.id | |
| 194 | 200 | } | |
| 201 | + | if s.kind == roleDragIcon && s.server.dnd != nil && s.server.dnd.icon == s { | |
| 202 | + | return s.server.dnd.iconID | |
| 203 | + | } | |
| 195 | 204 | return 0 | |
| 196 | 205 | } | |
| 197 | 206 | ||
| @@ -343,10 +352,17 @@ func (s *surface) Destroy(*wayland.Surface) { | |||
| 343 | 352 | if c := s.server.constraints[s]; c != nil { | |
| 344 | 353 | s.server.dropConstraint(c) | |
| 345 | 354 | } | |
| 355 | + | s.server.dndSurfaceGone(s) | |
| 346 | 356 | } | |
| 347 | - | func (s *surface) Attach(_ *wayland.Surface, b *wayland.Buffer, _, _ int32) { | |
| 357 | + | ||
| 358 | + | // Attach: before wl_surface v5 the offset rides on attach; only a drag | |
| 359 | + | // icon uses it. | |
| 360 | + | func (s *surface) Attach(r *wayland.Surface, b *wayland.Buffer, dx, dy int32) { | |
| 348 | 361 | s.next.buffer = b | |
| 349 | 362 | s.next.attached = true | |
| 363 | + | if r.Version() < 5 { | |
| 364 | + | s.next.dx, s.next.dy = dx, dy | |
| 365 | + | } | |
| 350 | 366 | } | |
| 351 | 367 | func (s *surface) Frame(r *wayland.Surface, id uint32) { | |
| 352 | 368 | cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{}) | |
| @@ -544,6 +560,9 @@ func (s *surface) applyCommit(u *update) { | |||
| 544 | 560 | } | |
| 545 | 561 | s.commitConstraint(u.cons, u.region, geometry) | |
| 546 | 562 | s.emitInput() | |
| 563 | + | if s.kind == roleDragIcon { | |
| 564 | + | s.server.dndIconCommit(s, u.dx, u.dy) | |
| 565 | + | } | |
| 547 | 566 | if s.xdg != nil && s.xdg.window != nil { | |
| 548 | 567 | s.xdg.window.afterCommit() | |
| 549 | 568 | } | |
| @@ -757,5 +776,9 @@ func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | |||
| 757 | 776 | } | |
| 758 | 777 | s.next.scale = int(v) | |
| 759 | 778 | } | |
| 760 | - | func (*surface) Offset(*wayland.Surface, int32, int32) {} | |
| 761 | - | func (*surface) GetRelease(*wayland.Surface, uint32) {} | |
| 779 | + | ||
| 780 | + | // Offset moves the next commit's surface (v5); only a drag icon uses it. | |
| 781 | + | func (s *surface) Offset(_ *wayland.Surface, dx, dy int32) { | |
| 782 | + | s.next.dx, s.next.dy = dx, dy | |
| 783 | + | } | |
| 784 | + | 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,10 @@ 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 are the last attach offset or wl_surface.offset request since | |||
| 143 | + | // the commit (the latest replaces the earlier); only a drag icon uses | |||
| 144 | + | // them. | |||
| 145 | + | dx, dy int32 | |||
| 140 | // damage is in surface (logical) pixels, bufDamage in buffer pixels. | 146 | // damage is in surface (logical) pixels, bufDamage in buffer pixels. | ||
| 141 | damage, bufDamage []ports.Rect | 147 | damage, bufDamage []ports.Rect | ||
| 142 | // feedback are wp_presentation feedbacks for this commit. | 148 | // feedback are wp_presentation feedbacks for this commit. | ||
| @@ -192,6 +198,9 @@ func (s *surface) windowID() ports.WindowID { | |||||
| 192 | if s.lock != nil && s.lock.mapped { | 198 | if s.lock != nil && s.lock.mapped { | ||
| 193 | return s.lock.id | 199 | return s.lock.id | ||
| 194 | } | 200 | } | ||
| 201 | + | if s.kind == roleDragIcon && s.server.dnd != nil && s.server.dnd.icon == s { | |||
| 202 | + | return s.server.dnd.iconID | |||
| 203 | + | } | |||
| 195 | return 0 | 204 | return 0 | ||
| 196 | } | 205 | } | ||
| 197 | 206 | ||||
| @@ -343,10 +352,17 @@ func (s *surface) Destroy(*wayland.Surface) { | |||||
| 343 | if c := s.server.constraints[s]; c != nil { | 352 | if c := s.server.constraints[s]; c != nil { | ||
| 344 | s.server.dropConstraint(c) | 353 | s.server.dropConstraint(c) | ||
| 345 | } | 354 | } | ||
| 355 | + | s.server.dndSurfaceGone(s) | |||
| 346 | } | 356 | } | ||
| 347 | - | func (s *surface) Attach(_ *wayland.Surface, b *wayland.Buffer, _, _ int32) { | 357 | + | |
| 358 | + | // Attach: before wl_surface v5 the offset rides on attach; only a drag | |||
| 359 | + | // icon uses it. | |||
| 360 | + | func (s *surface) Attach(r *wayland.Surface, b *wayland.Buffer, dx, dy int32) { | |||
| 348 | s.next.buffer = b | 361 | s.next.buffer = b | ||
| 349 | s.next.attached = true | 362 | s.next.attached = true | ||
| 363 | + | if r.Version() < 5 { | |||
| 364 | + | s.next.dx, s.next.dy = dx, dy | |||
| 365 | + | } | |||
| 350 | } | 366 | } | ||
| 351 | func (s *surface) Frame(r *wayland.Surface, id uint32) { | 367 | func (s *surface) Frame(r *wayland.Surface, id uint32) { | ||
| 352 | cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{}) | 368 | cb, err := wayland.NewCallback(r.Client(), 1, id, struct{}{}) | ||
| @@ -544,6 +560,9 @@ func (s *surface) applyCommit(u *update) { | |||||
| 544 | } | 560 | } | ||
| 545 | s.commitConstraint(u.cons, u.region, geometry) | 561 | s.commitConstraint(u.cons, u.region, geometry) | ||
| 546 | s.emitInput() | 562 | s.emitInput() | ||
| 563 | + | if s.kind == roleDragIcon { | |||
| 564 | + | s.server.dndIconCommit(s, u.dx, u.dy) | |||
| 565 | + | } | |||
| 547 | if s.xdg != nil && s.xdg.window != nil { | 566 | if s.xdg != nil && s.xdg.window != nil { | ||
| 548 | s.xdg.window.afterCommit() | 567 | s.xdg.window.afterCommit() | ||
| 549 | } | 568 | } | ||
| @@ -757,5 +776,9 @@ func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | |||||
| 757 | } | 776 | } | ||
| 758 | s.next.scale = int(v) | 777 | s.next.scale = int(v) | ||
| 759 | } | 778 | } | ||
| 760 | - | func (*surface) Offset(*wayland.Surface, int32, int32) {} | 779 | + | |
| 761 | - | func (*surface) GetRelease(*wayland.Surface, uint32) {} | 780 | + | // Offset moves the next commit's surface (v5); only a drag icon uses it. |
| 781 | + | func (s *surface) Offset(_ *wayland.Surface, dx, dy int32) { | |||
| 782 | + | s.next.dx, s.next.dy = dx, dy | |||
| 783 | + | } | |||
| 784 | + | func (*surface) GetRelease(*wayland.Surface, uint32) {} | |||
M internal/core/client.go +21 -0
| @@ -84,6 +84,24 @@ func (c *Core) clientEvent(ctx context.Context, ev ports.ClientEvent) (publish, | |||
| 84 | 84 | if c.clientDrag(ctx, v) != nil { | |
| 85 | 85 | return false, true | |
| 86 | 86 | } | |
| 87 | + | case ports.DataDragStarted: | |
| 88 | + | if c.startDataDrag(ctx, v) != nil { | |
| 89 | + | return false, true | |
| 90 | + | } | |
| 91 | + | return false, false | |
| 92 | + | case ports.DataDragIcon: | |
| 93 | + | if !c.setDataDragIcon(v) { | |
| 94 | + | return false, false | |
| 95 | + | } | |
| 96 | + | case ports.DataDragEnded: | |
| 97 | + | if c.dataDrag == nil { | |
| 98 | + | return false, false | |
| 99 | + | } | |
| 100 | + | c.abortDataDrag() | |
| 101 | + | if c.finishDataDrag(ctx) != nil { | |
| 102 | + | return false, true | |
| 103 | + | } | |
| 104 | + | return false, false | |
| 87 | 105 | case ports.WindowFullscreenRequest: | |
| 88 | 106 | if c.fullscreenRequest(ctx, v) != nil { | |
| 89 | 107 | return false, true | |
| @@ -114,6 +132,9 @@ func (c *Core) unmapClient(ctx context.Context, v ports.WindowUnmapped) error { | |||
| 114 | 132 | if c.drag != nil && c.drag.id == v.ID { | |
| 115 | 133 | c.abortDrag() | |
| 116 | 134 | } | |
| 135 | + | if err := c.dataDragGone(ctx, v.ID); err != nil { | |
| 136 | + | return err | |
| 137 | + | } | |
| 117 | 138 | c.unmapWindow(v) | |
| 118 | 139 | c.releaseSlots() | |
| 119 | 140 | if c.pointer == v.ID { | |
| @@ -84,6 +84,24 @@ func (c *Core) clientEvent(ctx context.Context, ev ports.ClientEvent) (publish, | |||||
| 84 | if c.clientDrag(ctx, v) != nil { | 84 | if c.clientDrag(ctx, v) != nil { | ||
| 85 | return false, true | 85 | return false, true | ||
| 86 | } | 86 | } | ||
| 87 | + | case ports.DataDragStarted: | |||
| 88 | + | if c.startDataDrag(ctx, v) != nil { | |||
| 89 | + | return false, true | |||
| 90 | + | } | |||
| 91 | + | return false, false | |||
| 92 | + | case ports.DataDragIcon: | |||
| 93 | + | if !c.setDataDragIcon(v) { | |||
| 94 | + | return false, false | |||
| 95 | + | } | |||
| 96 | + | case ports.DataDragEnded: | |||
| 97 | + | if c.dataDrag == nil { | |||
| 98 | + | return false, false | |||
| 99 | + | } | |||
| 100 | + | c.abortDataDrag() | |||
| 101 | + | if c.finishDataDrag(ctx) != nil { | |||
| 102 | + | return false, true | |||
| 103 | + | } | |||
| 104 | + | return false, false | |||
| 87 | case ports.WindowFullscreenRequest: | 105 | case ports.WindowFullscreenRequest: | ||
| 88 | if c.fullscreenRequest(ctx, v) != nil { | 106 | if c.fullscreenRequest(ctx, v) != nil { | ||
| 89 | return false, true | 107 | return false, true | ||
| @@ -114,6 +132,9 @@ func (c *Core) unmapClient(ctx context.Context, v ports.WindowUnmapped) error { | |||||
| 114 | if c.drag != nil && c.drag.id == v.ID { | 132 | if c.drag != nil && c.drag.id == v.ID { | ||
| 115 | c.abortDrag() | 133 | c.abortDrag() | ||
| 116 | } | 134 | } | ||
| 135 | + | if err := c.dataDragGone(ctx, v.ID); err != nil { | |||
| 136 | + | return err | |||
| 137 | + | } | |||
| 117 | c.unmapWindow(v) | 138 | c.unmapWindow(v) | ||
| 118 | c.releaseSlots() | 139 | c.releaseSlots() | ||
| 119 | if c.pointer == v.ID { | 140 | if c.pointer == v.ID { | ||
M internal/core/clientgesture.go +1 -1
| @@ -38,7 +38,7 @@ func (c *Core) gestureBegin(ctx context.Context, kind ports.GestureKind, fingers | |||
| 38 | 38 | return err | |
| 39 | 39 | } | |
| 40 | 40 | c.gesture = clientGesture{on: true, kind: kind, at: at} | |
| 41 | - | if c.pointer == 0 || c.drag != nil { | |
| 41 | + | if c.pointer == 0 || c.drag != nil || c.dataDrag != nil { | |
| 42 | 42 | return nil | |
| 43 | 43 | } | |
| 44 | 44 | c.gesture.id = c.pointer | |
| @@ -38,7 +38,7 @@ func (c *Core) gestureBegin(ctx context.Context, kind ports.GestureKind, fingers | |||||
| 38 | return err | 38 | return err | ||
| 39 | } | 39 | } | ||
| 40 | c.gesture = clientGesture{on: true, kind: kind, at: at} | 40 | c.gesture = clientGesture{on: true, kind: kind, at: at} | ||
| 41 | - | if c.pointer == 0 || c.drag != nil { | 41 | + | if c.pointer == 0 || c.drag != nil || c.dataDrag != nil { |
| 42 | return nil | 42 | return nil | ||
| 43 | } | 43 | } | ||
| 44 | c.gesture.id = c.pointer | 44 | c.gesture.id = c.pointer | ||
M internal/core/core.go +7 -1
| @@ -101,7 +101,9 @@ type Core struct { | |||
| 101 | 101 | // buttons whose release no client must see. mods are the modifiers | |
| 102 | 102 | // held, cmdMod the one keyboard.cmd names. lastButton is the latest | |
| 103 | 103 | // press a client saw, the one xdg_toplevel.move's serial names. | |
| 104 | - | drag *dragState | |
| 104 | + | drag *dragState | |
| 105 | + | // dataDrag is the client data drag in progress (datadrag.go). | |
| 106 | + | dataDrag *dataDragState | |
| 105 | 107 | swallow map[uint32]bool | |
| 106 | 108 | lastButton uint32 | |
| 107 | 109 | mods ports.Mods | |
| @@ -760,6 +762,10 @@ func (c *Core) slidFrame(ctx context.Context, shown bool, only *screen) error { | |||
| 760 | 762 | // rehit points the pointer at what now lies under a still cursor, as | |
| 761 | 763 | // windows slide under it. A held button keeps its grab. | |
| 762 | 764 | func (c *Core) rehit(ctx context.Context) error { | |
| 765 | + | if c.dataDrag != nil { | |
| 766 | + | // The drag target follows the layout too. | |
| 767 | + | return c.dataDragRetarget(ctx, c.motionTime) | |
| 768 | + | } | |
| 763 | 769 | if c.grab != 0 || c.drag != nil { | |
| 764 | 770 | return nil | |
| 765 | 771 | } | |
| @@ -101,7 +101,9 @@ type Core struct { | |||||
| 101 | // buttons whose release no client must see. mods are the modifiers | 101 | // buttons whose release no client must see. mods are the modifiers | ||
| 102 | // held, cmdMod the one keyboard.cmd names. lastButton is the latest | 102 | // held, cmdMod the one keyboard.cmd names. lastButton is the latest | ||
| 103 | // press a client saw, the one xdg_toplevel.move's serial names. | 103 | // press a client saw, the one xdg_toplevel.move's serial names. | ||
| 104 | - | drag *dragState | 104 | + | drag *dragState |
| 105 | + | // dataDrag is the client data drag in progress (datadrag.go). | |||
| 106 | + | dataDrag *dataDragState | |||
| 105 | swallow map[uint32]bool | 107 | swallow map[uint32]bool | ||
| 106 | lastButton uint32 | 108 | lastButton uint32 | ||
| 107 | mods ports.Mods | 109 | mods ports.Mods | ||
| @@ -760,6 +762,10 @@ func (c *Core) slidFrame(ctx context.Context, shown bool, only *screen) error { | |||||
| 760 | // rehit points the pointer at what now lies under a still cursor, as | 762 | // rehit points the pointer at what now lies under a still cursor, as | ||
| 761 | // windows slide under it. A held button keeps its grab. | 763 | // windows slide under it. A held button keeps its grab. | ||
| 762 | func (c *Core) rehit(ctx context.Context) error { | 764 | func (c *Core) rehit(ctx context.Context) error { | ||
| 765 | + | if c.dataDrag != nil { | |||
| 766 | + | // The drag target follows the layout too. | |||
| 767 | + | return c.dataDragRetarget(ctx, c.motionTime) | |||
| 768 | + | } | |||
| 763 | if c.grab != 0 || c.drag != nil { | 769 | if c.grab != 0 || c.drag != nil { | ||
| 764 | return nil | 770 | return nil | ||
| 765 | } | 771 | } | ||
M internal/core/core_test.go +1 -1
| @@ -307,7 +307,7 @@ func TestPointerFocusAndGrab(t *testing.T) { | |||
| 307 | 307 | t.Fatal(v) | |
| 308 | 308 | } | |
| 309 | 309 | input <- ports.PointerButton{Button: 0x110, Pressed: true} | |
| 310 | - | if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true}) { | |
| 310 | + | if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true, Grab: true}) { | |
| 311 | 311 | t.Fatal(v) | |
| 312 | 312 | } | |
| 313 | 313 | scene(t, scenes) | |
| @@ -307,7 +307,7 @@ func TestPointerFocusAndGrab(t *testing.T) { | |||||
| 307 | t.Fatal(v) | 307 | t.Fatal(v) | ||
| 308 | } | 308 | } | ||
| 309 | input <- ports.PointerButton{Button: 0x110, Pressed: true} | 309 | input <- ports.PointerButton{Button: 0x110, Pressed: true} | ||
| 310 | - | if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true}) { | 310 | + | if v := command(t, commands); v != (ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true, Grab: true}) { |
| 311 | t.Fatal(v) | 311 | t.Fatal(v) | ||
| 312 | } | 312 | } | ||
| 313 | scene(t, scenes) | 313 | scene(t, scenes) | ||
A internal/core/datadrag.go +206 -0
| @@ -0,0 +1,206 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // Data drag: a client drag and drop (wl_data_device.start_drag). Wayland | |
| 11 | + | // validated the request against the client's press and tells core; core | |
| 12 | + | // owns the pointer for the length of the drag, as for a window drag | |
| 13 | + | // (drag.go): | |
| 14 | + | // | |
| 15 | + | // - the origin client gets the pointer leave and no pointer event after; | |
| 16 | + | // nobody else gets one: the surface under the pointer is the drag | |
| 17 | + | // target, told to wayland as DataDragFocus and DataDragMotion; | |
| 18 | + | // - releasing the drag button drops (DataDragDrop), Escape cancels | |
| 19 | + | // (DataDragCancel); neither reaches a client; | |
| 20 | + | // - scrolling and touchpad gestures go nowhere, compositor swipes | |
| 21 | + | // included; binds still run, Escape cancels the drag before the | |
| 22 | + | // switcher; the keyboard keeps its | |
| 23 | + | // focus, clients read the modifiers that choose the action from it; | |
| 24 | + | // - the drag icon, a client surface wayland measures (DataDragIcon), is | |
| 25 | + | // drawn at the cursor over everything. | |
| 26 | + | // | |
| 27 | + | // Layout changes under a still cursor reroute the drag (rehit), and the | |
| 28 | + | // drop re-hits. | |
| 29 | + | ||
| 30 | + | // dataDragState is the data drag in progress. | |
| 31 | + | type dataDragState struct { | |
| 32 | + | // button is the pointer button that holds the drag. | |
| 33 | + | button uint32 | |
| 34 | + | // target is the surface the drag was last routed to (0: none), at | |
| 35 | + | // at in its coordinates. | |
| 36 | + | target WindowID | |
| 37 | + | at [2]float64 | |
| 38 | + | icon dataDragIcon | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // dataDragIcon is the icon surface: its top left corner relative to the | |
| 42 | + | // cursor and its size, logical px. Hidden while W or H is 0. | |
| 43 | + | type dataDragIcon struct { | |
| 44 | + | id WindowID | |
| 45 | + | x, y, w, h int | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // startDataDrag answers the start_drag wayland validated: it takes the | |
| 49 | + | // pointer if a client holds a button, else cancels the drag. A drag | |
| 50 | + | // during a window drag or a locked session is refused. | |
| 51 | + | func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error { | |
| 52 | + | button := v.Button | |
| 53 | + | if c.dataDrag != nil || c.drag != nil || c.security.Protected || c.grab == 0 || c.held() == 0 || !c.buttons[button] || c.swallow[button] { | |
| 54 | + | return c.command(ctx, ports.DataDragCancel{}) | |
| 55 | + | } | |
| 56 | + | c.dataDrag = &dataDragState{button: button, icon: dataDragIcon{id: v.Icon}} | |
| 57 | + | // Other held buttons release without a client to tell. | |
| 58 | + | for b := range c.buttons { | |
| 59 | + | if b != button { | |
| 60 | + | c.swallow[b] = true | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | c.grab = 0 | |
| 64 | + | // A compositor swipe in progress lets go of what it moved. | |
| 65 | + | if c.swipe != nil { | |
| 66 | + | c.dropSwipe() | |
| 67 | + | c.swipe = nil | |
| 68 | + | } | |
| 69 | + | // The origin gets its pointer leave, as the drag takes the pointer. | |
| 70 | + | if c.pointer != 0 { | |
| 71 | + | c.pointer = 0 | |
| 72 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { | |
| 73 | + | return err | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | // The surface under the pointer, the origin's usually, is the first | |
| 77 | + | // target. | |
| 78 | + | return c.dataDragRoute(ctx, c.motionTime) | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // dataDragRoute sends the surface under the cursor as the drag target: a | |
| 82 | + | // focus when it changed, else a motion. | |
| 83 | + | func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error { | |
| 84 | + | d := c.dataDrag | |
| 85 | + | id, x, y := c.hit(c.cursorX, c.cursorY) | |
| 86 | + | d.at = [2]float64{x, y} | |
| 87 | + | switch { | |
| 88 | + | case id != d.target: | |
| 89 | + | d.target = id | |
| 90 | + | return c.command(ctx, ports.DataDragFocus{ID: id, X: x, Y: y, Time: at}) | |
| 91 | + | case id != 0: | |
| 92 | + | return c.command(ctx, ports.DataDragMotion{ID: id, X: x, Y: y, Time: at}) | |
| 93 | + | } | |
| 94 | + | return nil | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // dataDragRetarget re-routes a still cursor: the target is told only when | |
| 98 | + | // it or the point in it changed. | |
| 99 | + | func (c *Core) dataDragRetarget(ctx context.Context, at time.Duration) error { | |
| 100 | + | d := c.dataDrag | |
| 101 | + | if id, x, y := c.hit(c.cursorX, c.cursorY); id == d.target && (id == 0 || d.at == [2]float64{x, y}) { | |
| 102 | + | return nil | |
| 103 | + | } | |
| 104 | + | return c.dataDragRoute(ctx, at) | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | // dataDragMotion follows the cursor: the target gets the motion and the | |
| 108 | + | // icon moves with it. | |
| 109 | + | func (c *Core) dataDragMotion(ctx context.Context, at time.Duration) error { | |
| 110 | + | if err := c.dataDragRoute(ctx, at); err != nil { | |
| 111 | + | return err | |
| 112 | + | } | |
| 113 | + | if c.dataDrag.icon.visible() { | |
| 114 | + | return c.publish(ctx) | |
| 115 | + | } | |
| 116 | + | return nil | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | // dataDragButton handles a button event of a data drag: the release of | |
| 120 | + | // the drag button drops; the other buttons do nothing, their releases | |
| 121 | + | // neither. | |
| 122 | + | func (c *Core) dataDragButton(ctx context.Context, v ports.PointerButton) error { | |
| 123 | + | if !v.Pressed { | |
| 124 | + | if v.Button != c.dataDrag.button { | |
| 125 | + | return nil | |
| 126 | + | } | |
| 127 | + | delete(c.buttons, v.Button) | |
| 128 | + | // A bind may have moved the layout under a still cursor: the | |
| 129 | + | // drop goes to what lies there now. | |
| 130 | + | if err := c.dataDragRetarget(ctx, v.Time); err != nil { | |
| 131 | + | return err | |
| 132 | + | } | |
| 133 | + | c.dataDrag = nil | |
| 134 | + | if err := c.command(ctx, ports.DataDragDrop{}); err != nil { | |
| 135 | + | return err | |
| 136 | + | } | |
| 137 | + | return c.finishDataDrag(ctx) | |
| 138 | + | } | |
| 139 | + | c.buttons[v.Button] = true | |
| 140 | + | c.swallow[v.Button] = true | |
| 141 | + | return nil | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | // cancelDataDrag ends the drag on Escape. | |
| 145 | + | func (c *Core) cancelDataDrag(ctx context.Context) error { | |
| 146 | + | c.abortDataDrag() | |
| 147 | + | if err := c.command(ctx, ports.DataDragCancel{}); err != nil { | |
| 148 | + | return err | |
| 149 | + | } | |
| 150 | + | return c.finishDataDrag(ctx) | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | // abortDataDrag ends the drag where it is; the held button's release is | |
| 154 | + | // dropped. | |
| 155 | + | func (c *Core) abortDataDrag() { | |
| 156 | + | if d := c.dataDrag; d != nil { | |
| 157 | + | c.swallow[d.button] = true | |
| 158 | + | c.dataDrag = nil | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | // finishDataDrag redraws without the icon and points the pointer at what | |
| 163 | + | // lies under the cursor. | |
| 164 | + | func (c *Core) finishDataDrag(ctx context.Context) error { | |
| 165 | + | if err := c.publish(ctx); err != nil { | |
| 166 | + | return err | |
| 167 | + | } | |
| 168 | + | return c.rehit(ctx) | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | // dataDragGone drops the target that was unmapped: it gets its leave. | |
| 172 | + | func (c *Core) dataDragGone(ctx context.Context, id WindowID) error { | |
| 173 | + | if d := c.dataDrag; d != nil && d.target == id { | |
| 174 | + | d.target = 0 | |
| 175 | + | return c.command(ctx, ports.DataDragFocus{Time: c.motionTime}) | |
| 176 | + | } | |
| 177 | + | return nil | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // setDataDragIcon records the icon geometry wayland measured. It reports | |
| 181 | + | // whether the scene changes. | |
| 182 | + | func (c *Core) setDataDragIcon(v ports.DataDragIcon) bool { | |
| 183 | + | d := c.dataDrag | |
| 184 | + | if d == nil || d.icon.id == 0 || v.ID != d.icon.id { | |
| 185 | + | return false | |
| 186 | + | } | |
| 187 | + | icon := dataDragIcon{id: v.ID, x: v.X, y: v.Y, w: max(v.W, 0), h: max(v.H, 0)} | |
| 188 | + | if icon == d.icon { | |
| 189 | + | return false | |
| 190 | + | } | |
| 191 | + | d.icon = icon | |
| 192 | + | return true | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | func (i dataDragIcon) visible() bool { return i.id != 0 && i.w > 0 && i.h > 0 } | |
| 196 | + | ||
| 197 | + | // dataDragIconOn is the icon as a window of screen sc (index i) when the | |
| 198 | + | // cursor is on it. | |
| 199 | + | func (c *Core) dataDragIconOn(i int, sc *screen) (ports.SceneWindow, bool) { | |
| 200 | + | d := c.dataDrag | |
| 201 | + | if d == nil || !d.icon.visible() || c.screenAt(c.cursorX, c.cursorY) != i { | |
| 202 | + | return ports.SceneWindow{}, false | |
| 203 | + | } | |
| 204 | + | x, y := int(c.cursorX)-sc.x+d.icon.x, int(c.cursorY)-sc.y+d.icon.y | |
| 205 | + | return ports.SceneWindow{ID: d.icon.id, Rect: Rect{X: x, Y: y, W: d.icon.w, H: d.icon.h}, Popup: true, OverLayers: true}, true | |
| 206 | + | } | |
| @@ -0,0 +1,206 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // Data drag: a client drag and drop (wl_data_device.start_drag). Wayland | |||
| 11 | + | // validated the request against the client's press and tells core; core | |||
| 12 | + | // owns the pointer for the length of the drag, as for a window drag | |||
| 13 | + | // (drag.go): | |||
| 14 | + | // | |||
| 15 | + | // - the origin client gets the pointer leave and no pointer event after; | |||
| 16 | + | // nobody else gets one: the surface under the pointer is the drag | |||
| 17 | + | // target, told to wayland as DataDragFocus and DataDragMotion; | |||
| 18 | + | // - releasing the drag button drops (DataDragDrop), Escape cancels | |||
| 19 | + | // (DataDragCancel); neither reaches a client; | |||
| 20 | + | // - scrolling and touchpad gestures go nowhere, compositor swipes | |||
| 21 | + | // included; binds still run, Escape cancels the drag before the | |||
| 22 | + | // switcher; the keyboard keeps its | |||
| 23 | + | // focus, clients read the modifiers that choose the action from it; | |||
| 24 | + | // - the drag icon, a client surface wayland measures (DataDragIcon), is | |||
| 25 | + | // drawn at the cursor over everything. | |||
| 26 | + | // | |||
| 27 | + | // Layout changes under a still cursor reroute the drag (rehit), and the | |||
| 28 | + | // drop re-hits. | |||
| 29 | + | ||||
| 30 | + | // dataDragState is the data drag in progress. | |||
| 31 | + | type dataDragState struct { | |||
| 32 | + | // button is the pointer button that holds the drag. | |||
| 33 | + | button uint32 | |||
| 34 | + | // target is the surface the drag was last routed to (0: none), at | |||
| 35 | + | // at in its coordinates. | |||
| 36 | + | target WindowID | |||
| 37 | + | at [2]float64 | |||
| 38 | + | icon dataDragIcon | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // dataDragIcon is the icon surface: its top left corner relative to the | |||
| 42 | + | // cursor and its size, logical px. Hidden while W or H is 0. | |||
| 43 | + | type dataDragIcon struct { | |||
| 44 | + | id WindowID | |||
| 45 | + | x, y, w, h int | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // startDataDrag answers the start_drag wayland validated: it takes the | |||
| 49 | + | // pointer if a client holds a button, else cancels the drag. A drag | |||
| 50 | + | // during a window drag or a locked session is refused. | |||
| 51 | + | func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error { | |||
| 52 | + | button := v.Button | |||
| 53 | + | if c.dataDrag != nil || c.drag != nil || c.security.Protected || c.grab == 0 || c.held() == 0 || !c.buttons[button] || c.swallow[button] { | |||
| 54 | + | return c.command(ctx, ports.DataDragCancel{}) | |||
| 55 | + | } | |||
| 56 | + | c.dataDrag = &dataDragState{button: button, icon: dataDragIcon{id: v.Icon}} | |||
| 57 | + | // Other held buttons release without a client to tell. | |||
| 58 | + | for b := range c.buttons { | |||
| 59 | + | if b != button { | |||
| 60 | + | c.swallow[b] = true | |||
| 61 | + | } | |||
| 62 | + | } | |||
| 63 | + | c.grab = 0 | |||
| 64 | + | // A compositor swipe in progress lets go of what it moved. | |||
| 65 | + | if c.swipe != nil { | |||
| 66 | + | c.dropSwipe() | |||
| 67 | + | c.swipe = nil | |||
| 68 | + | } | |||
| 69 | + | // The origin gets its pointer leave, as the drag takes the pointer. | |||
| 70 | + | if c.pointer != 0 { | |||
| 71 | + | c.pointer = 0 | |||
| 72 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { | |||
| 73 | + | return err | |||
| 74 | + | } | |||
| 75 | + | } | |||
| 76 | + | // The surface under the pointer, the origin's usually, is the first | |||
| 77 | + | // target. | |||
| 78 | + | return c.dataDragRoute(ctx, c.motionTime) | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | // dataDragRoute sends the surface under the cursor as the drag target: a | |||
| 82 | + | // focus when it changed, else a motion. | |||
| 83 | + | func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error { | |||
| 84 | + | d := c.dataDrag | |||
| 85 | + | id, x, y := c.hit(c.cursorX, c.cursorY) | |||
| 86 | + | d.at = [2]float64{x, y} | |||
| 87 | + | switch { | |||
| 88 | + | case id != d.target: | |||
| 89 | + | d.target = id | |||
| 90 | + | return c.command(ctx, ports.DataDragFocus{ID: id, X: x, Y: y, Time: at}) | |||
| 91 | + | case id != 0: | |||
| 92 | + | return c.command(ctx, ports.DataDragMotion{ID: id, X: x, Y: y, Time: at}) | |||
| 93 | + | } | |||
| 94 | + | return nil | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // dataDragRetarget re-routes a still cursor: the target is told only when | |||
| 98 | + | // it or the point in it changed. | |||
| 99 | + | func (c *Core) dataDragRetarget(ctx context.Context, at time.Duration) error { | |||
| 100 | + | d := c.dataDrag | |||
| 101 | + | if id, x, y := c.hit(c.cursorX, c.cursorY); id == d.target && (id == 0 || d.at == [2]float64{x, y}) { | |||
| 102 | + | return nil | |||
| 103 | + | } | |||
| 104 | + | return c.dataDragRoute(ctx, at) | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | // dataDragMotion follows the cursor: the target gets the motion and the | |||
| 108 | + | // icon moves with it. | |||
| 109 | + | func (c *Core) dataDragMotion(ctx context.Context, at time.Duration) error { | |||
| 110 | + | if err := c.dataDragRoute(ctx, at); err != nil { | |||
| 111 | + | return err | |||
| 112 | + | } | |||
| 113 | + | if c.dataDrag.icon.visible() { | |||
| 114 | + | return c.publish(ctx) | |||
| 115 | + | } | |||
| 116 | + | return nil | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | // dataDragButton handles a button event of a data drag: the release of | |||
| 120 | + | // the drag button drops; the other buttons do nothing, their releases | |||
| 121 | + | // neither. | |||
| 122 | + | func (c *Core) dataDragButton(ctx context.Context, v ports.PointerButton) error { | |||
| 123 | + | if !v.Pressed { | |||
| 124 | + | if v.Button != c.dataDrag.button { | |||
| 125 | + | return nil | |||
| 126 | + | } | |||
| 127 | + | delete(c.buttons, v.Button) | |||
| 128 | + | // A bind may have moved the layout under a still cursor: the | |||
| 129 | + | // drop goes to what lies there now. | |||
| 130 | + | if err := c.dataDragRetarget(ctx, v.Time); err != nil { | |||
| 131 | + | return err | |||
| 132 | + | } | |||
| 133 | + | c.dataDrag = nil | |||
| 134 | + | if err := c.command(ctx, ports.DataDragDrop{}); err != nil { | |||
| 135 | + | return err | |||
| 136 | + | } | |||
| 137 | + | return c.finishDataDrag(ctx) | |||
| 138 | + | } | |||
| 139 | + | c.buttons[v.Button] = true | |||
| 140 | + | c.swallow[v.Button] = true | |||
| 141 | + | return nil | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | // cancelDataDrag ends the drag on Escape. | |||
| 145 | + | func (c *Core) cancelDataDrag(ctx context.Context) error { | |||
| 146 | + | c.abortDataDrag() | |||
| 147 | + | if err := c.command(ctx, ports.DataDragCancel{}); err != nil { | |||
| 148 | + | return err | |||
| 149 | + | } | |||
| 150 | + | return c.finishDataDrag(ctx) | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | // abortDataDrag ends the drag where it is; the held button's release is | |||
| 154 | + | // dropped. | |||
| 155 | + | func (c *Core) abortDataDrag() { | |||
| 156 | + | if d := c.dataDrag; d != nil { | |||
| 157 | + | c.swallow[d.button] = true | |||
| 158 | + | c.dataDrag = nil | |||
| 159 | + | } | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | // finishDataDrag redraws without the icon and points the pointer at what | |||
| 163 | + | // lies under the cursor. | |||
| 164 | + | func (c *Core) finishDataDrag(ctx context.Context) error { | |||
| 165 | + | if err := c.publish(ctx); err != nil { | |||
| 166 | + | return err | |||
| 167 | + | } | |||
| 168 | + | return c.rehit(ctx) | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | // dataDragGone drops the target that was unmapped: it gets its leave. | |||
| 172 | + | func (c *Core) dataDragGone(ctx context.Context, id WindowID) error { | |||
| 173 | + | if d := c.dataDrag; d != nil && d.target == id { | |||
| 174 | + | d.target = 0 | |||
| 175 | + | return c.command(ctx, ports.DataDragFocus{Time: c.motionTime}) | |||
| 176 | + | } | |||
| 177 | + | return nil | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | // setDataDragIcon records the icon geometry wayland measured. It reports | |||
| 181 | + | // whether the scene changes. | |||
| 182 | + | func (c *Core) setDataDragIcon(v ports.DataDragIcon) bool { | |||
| 183 | + | d := c.dataDrag | |||
| 184 | + | if d == nil || d.icon.id == 0 || v.ID != d.icon.id { | |||
| 185 | + | return false | |||
| 186 | + | } | |||
| 187 | + | icon := dataDragIcon{id: v.ID, x: v.X, y: v.Y, w: max(v.W, 0), h: max(v.H, 0)} | |||
| 188 | + | if icon == d.icon { | |||
| 189 | + | return false | |||
| 190 | + | } | |||
| 191 | + | d.icon = icon | |||
| 192 | + | return true | |||
| 193 | + | } | |||
| 194 | + | ||||
| 195 | + | func (i dataDragIcon) visible() bool { return i.id != 0 && i.w > 0 && i.h > 0 } | |||
| 196 | + | ||||
| 197 | + | // dataDragIconOn is the icon as a window of screen sc (index i) when the | |||
| 198 | + | // cursor is on it. | |||
| 199 | + | func (c *Core) dataDragIconOn(i int, sc *screen) (ports.SceneWindow, bool) { | |||
| 200 | + | d := c.dataDrag | |||
| 201 | + | if d == nil || !d.icon.visible() || c.screenAt(c.cursorX, c.cursorY) != i { | |||
| 202 | + | return ports.SceneWindow{}, false | |||
| 203 | + | } | |||
| 204 | + | x, y := int(c.cursorX)-sc.x+d.icon.x, int(c.cursorY)-sc.y+d.icon.y | |||
| 205 | + | return ports.SceneWindow{ID: d.icon.id, Rect: Rect{X: x, Y: y, W: d.icon.w, H: d.icon.h}, Popup: true, OverLayers: true}, true | |||
| 206 | + | } | |||
A internal/core/datadrag_test.go +577 -0
| @@ -0,0 +1,577 @@ | |||
| 1 | + | package core_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "slices" | |
| 5 | + | "testing" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | const dndIcon ports.WindowID = 900 | |
| 12 | + | ||
| 13 | + | // dndRig is a drag rig with tiles 1 and 2 side by side and the pointer | |
| 14 | + | // on 1 with its button held, as a client that is about to start_drag. | |
| 15 | + | type dndRig struct { | |
| 16 | + | *dragRig | |
| 17 | + | one, two ports.Rect | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | func startDnd(t *testing.T, outs ...ports.OutputInfo) *dndRig { | |
| 21 | + | t.Helper() | |
| 22 | + | if len(outs) == 0 { | |
| 23 | + | outs = []ports.OutputInfo{dragOut} | |
| 24 | + | } | |
| 25 | + | r := startDragRig(t, nil, outs...) | |
| 26 | + | r.client <- ports.WindowMapped{ID: 1} | |
| 27 | + | r.client <- ports.WindowMapped{ID: 2} | |
| 28 | + | set := r.settle(t) | |
| 29 | + | d := &dndRig{dragRig: r, one: dragRect(t, set, outs[0].Name, 1), two: dragRect(t, set, outs[0].Name, 2)} | |
| 30 | + | r.moveTo(float64(d.one.X+20), float64(d.one.Y+30), 1) | |
| 31 | + | r.in(ports.PointerButton{Button: 0x110, Pressed: true}) | |
| 32 | + | r.settle(t) | |
| 33 | + | r.drain() | |
| 34 | + | return d | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // dataDragCommands keeps what the data drag sends and the pointer events | |
| 38 | + | // no client may see during it. | |
| 39 | + | func dataDragCommands(cmds []ports.ClientCommand) []ports.ClientCommand { | |
| 40 | + | var out []ports.ClientCommand | |
| 41 | + | for _, v := range cmds { | |
| 42 | + | switch v.(type) { | |
| 43 | + | case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel, | |
| 44 | + | ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, | |
| 45 | + | ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo: | |
| 46 | + | out = append(out, v) | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | return out | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func (d *dndRig) start(t *testing.T, icon ports.WindowID) []ports.ClientCommand { | |
| 53 | + | t.Helper() | |
| 54 | + | d.client <- ports.DataDragStarted{Button: 0x110, Icon: icon} | |
| 55 | + | d.settle(t) | |
| 56 | + | return dataDragCommands(d.drain()) | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | func wantCommands(t *testing.T, got []ports.ClientCommand, want ...ports.ClientCommand) { | |
| 60 | + | t.Helper() | |
| 61 | + | if !slices.Equal(got, want) { | |
| 62 | + | t.Fatalf("commands %#v, want %#v", got, want) | |
| 63 | + | } | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | func TestDataDragRoutesToSurfaceUnderPointer(t *testing.T) { | |
| 67 | + | d := startDnd(t) | |
| 68 | + | // The origin loses the pointer and is the first target. | |
| 69 | + | wantCommands(t, d.start(t, 0), | |
| 70 | + | ports.PointerFocus{}, | |
| 71 | + | ports.DataDragFocus{ID: 1, X: 20, Y: 30, Time: time.Millisecond}) | |
| 72 | + | // Motion over the same surface. | |
| 73 | + | d.moveTo(float64(d.one.X+25), float64(d.one.Y+35), 2) | |
| 74 | + | d.settle(t) | |
| 75 | + | wantCommands(t, dataDragCommands(d.drain()), ports.DataDragMotion{ID: 1, X: 25, Y: 35, Time: 2 * time.Millisecond}) | |
| 76 | + | // Over the other window: it is the new target. | |
| 77 | + | d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3) | |
| 78 | + | d.settle(t) | |
| 79 | + | wantCommands(t, dataDragCommands(d.drain()), ports.DataDragFocus{ID: 2, X: 5, Y: 6, Time: 3 * time.Millisecond}) | |
| 80 | + | d.moveTo(float64(d.two.X+7), float64(d.two.Y+8), 4) | |
| 81 | + | d.settle(t) | |
| 82 | + | wantCommands(t, dataDragCommands(d.drain()), ports.DataDragMotion{ID: 2, X: 7, Y: 8, Time: 4 * time.Millisecond}) | |
| 83 | + | // Over nothing: no target. | |
| 84 | + | d.moveTo(float64(d.two.X+d.two.W+5), float64(d.two.Y+8), 5) | |
| 85 | + | d.settle(t) | |
| 86 | + | wantCommands(t, dataDragCommands(d.drain()), ports.DataDragFocus{Time: 5 * time.Millisecond}) | |
| 87 | + | d.moveTo(float64(d.two.X+d.two.W+6), float64(d.two.Y+8), 6) | |
| 88 | + | d.settle(t) | |
| 89 | + | wantCommands(t, dataDragCommands(d.drain())) | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | func TestDataDragDropsOnRelease(t *testing.T) { | |
| 93 | + | d := startDnd(t) | |
| 94 | + | d.start(t, 0) | |
| 95 | + | d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3) | |
| 96 | + | d.in(ports.PointerButton{Button: 0x110, Time: 4 * time.Millisecond}) | |
| 97 | + | d.settle(t) | |
| 98 | + | // The release is the drop; the pointer then points at the window under | |
| 99 | + | // the cursor, not at the origin that grabbed it. | |
| 100 | + | wantCommands(t, dataDragCommands(d.drain()), | |
| 101 | + | ports.DataDragFocus{ID: 2, X: 5, Y: 6, Time: 3 * time.Millisecond}, | |
| 102 | + | ports.DataDragDrop{}, | |
| 103 | + | ports.PointerFocus{ID: 2, X: 5, Y: 6}) | |
| 104 | + | // The drag is over: motion is plain pointer motion again. | |
| 105 | + | d.moveTo(float64(d.two.X+9), float64(d.two.Y+9), 5) | |
| 106 | + | d.settle(t) | |
| 107 | + | wantCommands(t, dataDragCommands(d.drain()), ports.PointerMotionTo{ID: 2, X: 9, Y: 9, Time: 5 * time.Millisecond}) | |
| 108 | + | // The next press is an ordinary click. | |
| 109 | + | d.in(ports.PointerButton{Button: 0x110, Pressed: true}) | |
| 110 | + | d.settle(t) | |
| 111 | + | wantCommands(t, dataDragCommands(d.drain()), ports.PointerButtonTo{ID: 2, Button: 0x110, Pressed: true, Grab: true}) | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | func TestDataDragSendsNothingToClients(t *testing.T) { | |
| 115 | + | d := startDnd(t) | |
| 116 | + | d.start(t, 0) | |
| 117 | + | d.in(ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 1}}) | |
| 118 | + | d.in(ports.PinchBegin{Fingers: 2, Time: time.Second}) | |
| 119 | + | d.in(ports.PinchUpdate{Scale: 2, Time: time.Second}) | |
| 120 | + | d.in(ports.PinchEnd{Time: time.Second}) | |
| 121 | + | d.in(ports.HoldBegin{Fingers: 2, Time: time.Second}) | |
| 122 | + | d.in(ports.HoldEnd{Time: time.Second}) | |
| 123 | + | d.in(ports.SwipeBegin{Fingers: 2, Time: time.Second}) | |
| 124 | + | d.in(ports.SwipeEnd{Time: time.Second}) | |
| 125 | + | // Other buttons neither press nor release anywhere. | |
| 126 | + | d.in(ports.PointerButton{Button: 0x111, Pressed: true}) | |
| 127 | + | d.in(ports.PointerButton{Button: 0x111}) | |
| 128 | + | d.settle(t) | |
| 129 | + | for _, v := range dataDragCommands(d.drain()) { | |
| 130 | + | switch v.(type) { | |
| 131 | + | case ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.PointerMotionTo, ports.DataDragDrop, ports.DataDragCancel: | |
| 132 | + | t.Fatalf("sent %#v", v) | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | // Still dragging: the drag button's release drops. | |
| 136 | + | d.in(ports.PointerButton{Button: 0x110}) | |
| 137 | + | d.settle(t) | |
| 138 | + | if got := dataDragCommands(d.drain()); len(got) == 0 || got[0] != (ports.DataDragDrop{}) { | |
| 139 | + | t.Fatalf("commands %#v", got) | |
| 140 | + | } | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | func TestDataDragKeyboardKeepsForwarding(t *testing.T) { | |
| 144 | + | d := startDnd(t) | |
| 145 | + | d.start(t, 0) | |
| 146 | + | d.in(ports.KeyEvent{Keysym: "Shift_L", Keycode: 42, Pressed: true, Mods: ports.ModShift}) | |
| 147 | + | d.in(ports.KeyEvent{Keysym: "Shift_L", Keycode: 42}) | |
| 148 | + | d.settle(t) | |
| 149 | + | n := 0 | |
| 150 | + | for _, v := range d.drain() { | |
| 151 | + | if _, ok := v.(ports.ForwardKey); ok { | |
| 152 | + | n++ | |
| 153 | + | } | |
| 154 | + | } | |
| 155 | + | if n != 2 { | |
| 156 | + | t.Fatalf("%d keys forwarded, want 2", n) | |
| 157 | + | } | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | func TestDataDragEscapeCancels(t *testing.T) { | |
| 161 | + | d := startDnd(t) | |
| 162 | + | d.start(t, 0) | |
| 163 | + | d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3) | |
| 164 | + | d.settle(t) | |
| 165 | + | d.drain() | |
| 166 | + | d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1, Pressed: true}) | |
| 167 | + | d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1}) | |
| 168 | + | d.in(ports.PointerButton{Button: 0x110}) | |
| 169 | + | d.settle(t) | |
| 170 | + | cmds := d.drain() | |
| 171 | + | wantCommands(t, dataDragCommands(cmds), | |
| 172 | + | ports.DataDragCancel{}, | |
| 173 | + | ports.PointerFocus{ID: 2, X: 5, Y: 6}) | |
| 174 | + | for _, v := range cmds { | |
| 175 | + | if k, ok := v.(ports.ForwardKey); ok { | |
| 176 | + | t.Fatalf("client saw %#v", k) | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | // The press that started the drag is over. | |
| 180 | + | d.moveTo(float64(d.two.X+9), float64(d.two.Y+9), 5) | |
| 181 | + | d.settle(t) | |
| 182 | + | wantCommands(t, dataDragCommands(d.drain()), ports.PointerMotionTo{ID: 2, X: 9, Y: 9, Time: 5 * time.Millisecond}) | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | func TestDataDragEndedByWayland(t *testing.T) { | |
| 186 | + | d := startDnd(t) | |
| 187 | + | d.start(t, dndIcon) | |
| 188 | + | d.client <- ports.DataDragIcon{ID: dndIcon, X: 1, Y: 2, W: 16, H: 16} | |
| 189 | + | d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3) | |
| 190 | + | d.settle(t) | |
| 191 | + | d.drain() | |
| 192 | + | // The source died: no command, the icon goes and the pointer is | |
| 193 | + | // pointed at the window under the cursor. | |
| 194 | + | d.client <- ports.DataDragEnded{} | |
| 195 | + | set := d.settle(t) | |
| 196 | + | if _, ok := windowIn(set, "A", dndIcon); ok { | |
| 197 | + | t.Fatal("icon still drawn") | |
| 198 | + | } | |
| 199 | + | wantCommands(t, dataDragCommands(d.drain()), | |
| 200 | + | ports.PointerFocus{ID: 2, X: 5, Y: 6}) | |
| 201 | + | // The held button's release reaches nobody. | |
| 202 | + | d.in(ports.PointerButton{Button: 0x110}) | |
| 203 | + | d.settle(t) | |
| 204 | + | wantCommands(t, dataDragCommands(d.drain())) | |
| 205 | + | // An end with no drag is ignored. | |
| 206 | + | d.client <- ports.DataDragEnded{} | |
| 207 | + | d.settle(t) | |
| 208 | + | wantCommands(t, dataDragCommands(d.drain())) | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | func TestDataDragRefused(t *testing.T) { | |
| 212 | + | t.Run("no press", func(t *testing.T) { | |
| 213 | + | d := startDnd(t) | |
| 214 | + | d.in(ports.PointerButton{Button: 0x110}) | |
| 215 | + | d.settle(t) | |
| 216 | + | d.drain() | |
| 217 | + | wantCommands(t, d.start(t, 0), ports.DataDragCancel{}) | |
| 218 | + | // No drag started: motion is plain. | |
| 219 | + | d.moveTo(float64(d.one.X+21), float64(d.one.Y+30), 2) | |
| 220 | + | d.settle(t) | |
| 221 | + | wantCommands(t, dataDragCommands(d.drain()), ports.PointerMotionTo{ID: 1, X: 21, Y: 30, Time: 2 * time.Millisecond}) | |
| 222 | + | }) | |
| 223 | + | t.Run("window drag", func(t *testing.T) { | |
| 224 | + | r := startDragRig(t, nil, dragOut) | |
| 225 | + | r.client <- ports.WindowMapped{ID: 1} | |
| 226 | + | r.client <- ports.WindowMapped{ID: 2} | |
| 227 | + | set := r.settle(t) | |
| 228 | + | one := dragRect(t, set, "A", 1) | |
| 229 | + | r.moveTo(float64(one.X+20), float64(one.Y+30), 1) | |
| 230 | + | r.settle(t) | |
| 231 | + | r.press(t, 0x110, ports.ModSuper) | |
| 232 | + | r.settle(t) | |
| 233 | + | r.drain() | |
| 234 | + | r.client <- ports.DataDragStarted{Button: 0x110} | |
| 235 | + | r.settle(t) | |
| 236 | + | wantCommands(t, r.drain(), ports.DataDragCancel{}) | |
| 237 | + | // The window drag goes on. | |
| 238 | + | r.moveTo(float64(one.X+200), float64(one.Y+30), 2) | |
| 239 | + | r.in(ports.PointerButton{Button: 0x110}) | |
| 240 | + | r.settle(t) | |
| 241 | + | wantCommands(t, dataDragCommands(r.drain()), ports.PointerFocus{ID: 1, X: 200, Y: 30}) | |
| 242 | + | }) | |
| 243 | + | t.Run("twice", func(t *testing.T) { | |
| 244 | + | d := startDnd(t) | |
| 245 | + | d.start(t, 0) | |
| 246 | + | wantCommands(t, d.start(t, 0), ports.DataDragCancel{}) | |
| 247 | + | }) | |
| 248 | + | t.Run("locked", func(t *testing.T) { | |
| 249 | + | d := startDnd(t) | |
| 250 | + | locked := ports.SecurityState{Generation: 1, Protected: true} | |
| 251 | + | d.epoch.Store(locked) | |
| 252 | + | d.client <- ports.SessionLockChanged{State: locked} | |
| 253 | + | d.settle(t) | |
| 254 | + | d.drain() | |
| 255 | + | d.client <- ports.DataDragStarted{Button: 0x110} | |
| 256 | + | d.settle(t) | |
| 257 | + | var got []ports.ClientCommand | |
| 258 | + | for _, v := range d.drain() { | |
| 259 | + | switch v.(type) { | |
| 260 | + | case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel: | |
| 261 | + | got = append(got, v) | |
| 262 | + | } | |
| 263 | + | } | |
| 264 | + | wantCommands(t, got, ports.DataDragCancel{}) | |
| 265 | + | }) | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | func TestDataDragCommandsAreSecurityWrapped(t *testing.T) { | |
| 269 | + | d := startDnd(t) | |
| 270 | + | d.client <- ports.DataDragStarted{Button: 0x110} | |
| 271 | + | d.settle(t) | |
| 272 | + | n := 0 | |
| 273 | + | for { | |
| 274 | + | select { | |
| 275 | + | case v := <-d.commands: | |
| 276 | + | s, ok := v.(ports.SecurityCommand) | |
| 277 | + | if !ok { | |
| 278 | + | t.Fatalf("unwrapped %#v", v) | |
| 279 | + | } | |
| 280 | + | if _, ok := s.Command.(ports.DataDragFocus); ok { | |
| 281 | + | n++ | |
| 282 | + | } | |
| 283 | + | continue | |
| 284 | + | default: | |
| 285 | + | } | |
| 286 | + | break | |
| 287 | + | } | |
| 288 | + | if n != 1 { | |
| 289 | + | t.Fatalf("%d DataDragFocus", n) | |
| 290 | + | } | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | func TestDataDragLockCancelsIt(t *testing.T) { | |
| 294 | + | d := startDnd(t) | |
| 295 | + | d.start(t, dndIcon) | |
| 296 | + | d.client <- ports.DataDragIcon{ID: dndIcon, W: 16, H: 16} | |
| 297 | + | d.settle(t) | |
| 298 | + | locked := ports.SecurityState{Generation: 1, Protected: true} | |
| 299 | + | d.epoch.Store(locked) | |
| 300 | + | d.client <- ports.SessionLockChanged{State: locked, Surfaces: []ports.LockSurfacePlacement{{ID: 10, Output: "A", Width: 1000, Height: 600}}} | |
| 301 | + | set := d.settle(t) | |
| 302 | + | if _, ok := windowIn(set, "A", dndIcon); ok { | |
| 303 | + | t.Fatal("icon drawn while locked") | |
| 304 | + | } | |
| 305 | + | d.drain() | |
| 306 | + | d.in(ports.PointerMotion{X: 100, Y: 100, Time: time.Second}) | |
| 307 | + | d.settle(t) | |
| 308 | + | for _, v := range d.drain() { | |
| 309 | + | switch v.(type) { | |
| 310 | + | case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop: | |
| 311 | + | t.Fatalf("data drag alive while locked: %#v", v) | |
| 312 | + | } | |
| 313 | + | } | |
| 314 | + | // After the unlock, the old drag is gone: plain pointer events. | |
| 315 | + | unlocked := ports.SecurityState{Generation: 2} | |
| 316 | + | d.epoch.Store(unlocked) | |
| 317 | + | d.client <- ports.SessionLockChanged{State: unlocked} | |
| 318 | + | d.settle(t) | |
| 319 | + | d.drain() | |
| 320 | + | d.moveTo(float64(d.one.X+21), float64(d.one.Y+30), 9) | |
| 321 | + | d.settle(t) | |
| 322 | + | got := dataDragCommands(d.drain()) | |
| 323 | + | wantCommands(t, got, ports.PointerFocus{ID: 1, X: 21, Y: 30}, ports.PointerMotionTo{ID: 1, X: 21, Y: 30, Time: 9 * time.Millisecond}) | |
| 324 | + | } | |
| 325 | + | ||
| 326 | + | func TestDataDragTargetUnmapped(t *testing.T) { | |
| 327 | + | d := startDnd(t) | |
| 328 | + | d.start(t, 0) | |
| 329 | + | d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3) | |
| 330 | + | d.settle(t) | |
| 331 | + | d.drain() | |
| 332 | + | d.client <- ports.WindowUnmapped{ID: 2} | |
| 333 | + | d.settle(t) | |
| 334 | + | cmds := dataDragCommands(d.drain()) | |
| 335 | + | if len(cmds) == 0 || cmds[0] != (ports.DataDragFocus{Time: 3 * time.Millisecond}) { | |
| 336 | + | t.Fatalf("commands %#v", cmds) | |
| 337 | + | } | |
| 338 | + | } | |
| 339 | + | ||
| 340 | + | func TestDataDragIconIsLastWindowAtCursor(t *testing.T) { | |
| 341 | + | d := startDnd(t) | |
| 342 | + | d.start(t, dndIcon) | |
| 343 | + | // Not drawn until wayland measured it. | |
| 344 | + | if _, ok := windowIn(d.settle(t), "A", dndIcon); ok { | |
| 345 | + | t.Fatal("icon drawn before its size") | |
| 346 | + | } | |
| 347 | + | cx, cy := d.one.X+20, d.one.Y+30 | |
| 348 | + | d.client <- ports.DataDragIcon{ID: dndIcon, X: -4, Y: -6, W: 32, H: 24} | |
| 349 | + | set := d.settle(t) | |
| 350 | + | s := dragScene(set, "A") | |
| 351 | + | last := s.Windows[len(s.Windows)-1] | |
| 352 | + | want := ports.SceneWindow{ID: dndIcon, Rect: ports.Rect{X: cx - 4, Y: cy - 6, W: 32, H: 24}, Popup: true, OverLayers: true} | |
| 353 | + | if last != want { | |
| 354 | + | t.Fatalf("last window %+v, want %+v", last, want) | |
| 355 | + | } | |
| 356 | + | // It follows the cursor; the scene changes. | |
| 357 | + | d.moveTo(float64(cx+100), float64(cy+50), 2) | |
| 358 | + | moved := dragScene(d.settle(t), "A") | |
| 359 | + | if moved.SameAs(s) || moved.Seq == s.Seq { | |
| 360 | + | t.Fatal("scene did not change with the icon") | |
| 361 | + | } | |
| 362 | + | if got := moved.Windows[len(moved.Windows)-1]; got.Rect.X != cx+96 || got.Rect.Y != cy+44 || got.ID != dndIcon { | |
| 363 | + | t.Fatalf("moved icon %+v", got) | |
| 364 | + | } | |
| 365 | + | // Under the icon, the surface below is still the target. | |
| 366 | + | d.drain() | |
| 367 | + | d.moveTo(float64(cx+101), float64(cy+50), 3) | |
| 368 | + | d.settle(t) | |
| 369 | + | for _, v := range d.drain() { | |
| 370 | + | if f, ok := v.(ports.DataDragFocus); ok && f.ID == dndIcon { | |
| 371 | + | t.Fatalf("icon was targeted: %#v", f) | |
| 372 | + | } | |
| 373 | + | } | |
| 374 | + | // A commit that changes the offset moves it; a zero size hides it. | |
| 375 | + | d.client <- ports.DataDragIcon{ID: dndIcon, X: 0, Y: 0, W: 32, H: 24} | |
| 376 | + | if got := dragScene(d.settle(t), "A"); got.Windows[len(got.Windows)-1].Rect.X != cx+101 { | |
| 377 | + | t.Fatalf("offset ignored: %+v", got.Windows[len(got.Windows)-1]) | |
| 378 | + | } | |
| 379 | + | d.client <- ports.DataDragIcon{ID: dndIcon} | |
| 380 | + | if _, ok := windowIn(d.settle(t), "A", dndIcon); ok { | |
| 381 | + | t.Fatal("hidden icon drawn") | |
| 382 | + | } | |
| 383 | + | // Geometry of another surface is not the icon's. | |
| 384 | + | d.client <- ports.DataDragIcon{ID: dndIcon + 1, W: 10, H: 10} | |
| 385 | + | if _, ok := windowIn(d.settle(t), "A", dndIcon+1); ok { | |
| 386 | + | t.Fatal("foreign icon drawn") | |
| 387 | + | } | |
| 388 | + | // The drop removes it. | |
| 389 | + | d.client <- ports.DataDragIcon{ID: dndIcon, W: 32, H: 24} | |
| 390 | + | d.settle(t) | |
| 391 | + | d.in(ports.PointerButton{Button: 0x110}) | |
| 392 | + | set = d.settle(t) | |
| 393 | + | if _, ok := windowIn(set, "A", dndIcon); ok { | |
| 394 | + | t.Fatal("icon drawn after the drop") | |
| 395 | + | } | |
| 396 | + | } | |
| 397 | + | ||
| 398 | + | func TestDataDragIconOnScreenUnderCursor(t *testing.T) { | |
| 399 | + | other := ports.OutputInfo{Name: "B", Width: 800, Height: 600} | |
| 400 | + | d := startDnd(t, dragOut, other) | |
| 401 | + | d.start(t, dndIcon) | |
| 402 | + | d.client <- ports.DataDragIcon{ID: dndIcon, X: 2, Y: 3, W: 10, H: 10} | |
| 403 | + | d.moveTo(1000+100, 50, 2) | |
| 404 | + | set := d.settle(t) | |
| 405 | + | if _, ok := windowIn(set, "A", dndIcon); ok { | |
| 406 | + | t.Fatal("icon on the other screen") | |
| 407 | + | } | |
| 408 | + | w, ok := windowIn(set, "B", dndIcon) | |
| 409 | + | if !ok { | |
| 410 | + | t.Fatal("no icon on B") | |
| 411 | + | } | |
| 412 | + | if w.Rect != (ports.Rect{X: 102, Y: 53, W: 10, H: 10}) { | |
| 413 | + | t.Fatalf("icon %+v", w.Rect) | |
| 414 | + | } | |
| 415 | + | } | |
| 416 | + | ||
| 417 | + | func TestDataDragNoIconIgnoresIconEvents(t *testing.T) { | |
| 418 | + | d := startDnd(t) | |
| 419 | + | d.start(t, 0) | |
| 420 | + | d.client <- ports.DataDragIcon{ID: dndIcon, W: 10, H: 10} | |
| 421 | + | if _, ok := windowIn(d.settle(t), "A", dndIcon); ok { | |
| 422 | + | t.Fatal("icon drawn for a drag without one") | |
| 423 | + | } | |
| 424 | + | // Nor with no drag at all. | |
| 425 | + | d.in(ports.PointerButton{Button: 0x110}) | |
| 426 | + | d.settle(t) | |
| 427 | + | d.client <- ports.DataDragIcon{ID: dndIcon, W: 10, H: 10} | |
| 428 | + | if _, ok := windowIn(d.settle(t), "A", dndIcon); ok { | |
| 429 | + | t.Fatal("icon drawn without a drag") | |
| 430 | + | } | |
| 431 | + | } | |
| 432 | + | ||
| 433 | + | func TestDataDragFocusScreenFollowsPointer(t *testing.T) { | |
| 434 | + | r := startDragRig(t, nil, dragOut, ports.OutputInfo{Name: "B", Width: 1000, Height: 600}) | |
| 435 | + | r.client <- ports.WindowMapped{ID: 1} | |
| 436 | + | r.settle(t) | |
| 437 | + | r.moveTo(1500, 300, 1) | |
| 438 | + | r.settle(t) | |
| 439 | + | r.client <- ports.WindowMapped{ID: 3} | |
| 440 | + | set := r.settle(t) | |
| 441 | + | one := dragRect(t, set, "A", 1) | |
| 442 | + | r.moveTo(float64(one.X+20), float64(one.Y+30), 2) | |
| 443 | + | set = r.settle(t) | |
| 444 | + | if w, _ := windowIn(set, "A", 1); !w.Focused { | |
| 445 | + | t.Fatal("1 not focused") | |
| 446 | + | } | |
| 447 | + | r.in(ports.PointerButton{Button: 0x110, Pressed: true}) | |
| 448 | + | r.settle(t) | |
| 449 | + | r.client <- ports.DataDragStarted{Button: 0x110} | |
| 450 | + | r.moveTo(1500, 300, 3) | |
| 451 | + | set = r.settle(t) | |
| 452 | + | if w, _ := windowIn(set, "B", 3); !w.Focused { | |
| 453 | + | t.Fatal("the screen under the drag did not take the focus") | |
| 454 | + | } | |
| 455 | + | three := dragRect(t, set, "B", 3) | |
| 456 | + | var focus []ports.DataDragFocus | |
| 457 | + | for _, v := range r.drain() { | |
| 458 | + | if f, ok := v.(ports.DataDragFocus); ok { | |
| 459 | + | focus = append(focus, f) | |
| 460 | + | } | |
| 461 | + | } | |
| 462 | + | if n := len(focus); n != 2 || focus[1].ID != 3 || focus[1].X != float64(1500-1000-three.X) { | |
| 463 | + | t.Fatalf("focus %+v", focus) | |
| 464 | + | } | |
| 465 | + | } | |
| 466 | + | ||
| 467 | + | func TestDataDragFollowsWorkspaceSwitchUnderStillCursor(t *testing.T) { | |
| 468 | + | d := startDnd(t) | |
| 469 | + | d.start(t, 0) | |
| 470 | + | d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3) | |
| 471 | + | d.settle(t) | |
| 472 | + | d.drain() | |
| 473 | + | // cmd+2 shows an empty workspace under the still cursor: the target | |
| 474 | + | // leaves, and the release drops on nothing. | |
| 475 | + | d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper, Pressed: true}) | |
| 476 | + | d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper}) | |
| 477 | + | d.settle(t) | |
| 478 | + | got := dataDragCommands(d.drain()) | |
| 479 | + | if !slices.Contains(got, ports.ClientCommand(ports.DataDragFocus{Time: 3 * time.Millisecond})) { | |
| 480 | + | t.Fatalf("no leave after the switch: %#v", got) | |
| 481 | + | } | |
| 482 | + | d.in(ports.PointerButton{Button: 0x110, Time: 4 * time.Millisecond}) | |
| 483 | + | d.settle(t) | |
| 484 | + | got = dataDragCommands(d.drain()) | |
| 485 | + | if len(got) == 0 || got[0] != (ports.DataDragDrop{}) { | |
| 486 | + | t.Fatalf("commands %#v", got) | |
| 487 | + | } | |
| 488 | + | for _, v := range got { | |
| 489 | + | if f, ok := v.(ports.DataDragFocus); ok && f.ID != 0 { | |
| 490 | + | t.Fatalf("hidden window targeted: %#v", got) | |
| 491 | + | } | |
| 492 | + | } | |
| 493 | + | } | |
| 494 | + | ||
| 495 | + | func TestDataDragTargetMovedUnderStillCursor(t *testing.T) { | |
| 496 | + | d := startDnd(t) | |
| 497 | + | d.start(t, 0) | |
| 498 | + | // Over 2, focused by a click-free keyboard move, then cmd+f maximizes | |
| 499 | + | // its column: still the target under the cursor, at another point. | |
| 500 | + | d.moveTo(float64(d.two.X+d.two.W-20), float64(d.two.Y+30), 2) | |
| 501 | + | d.in(ports.KeyEvent{Keysym: "l", Keycode: 38, Mods: ports.ModSuper, Pressed: true}) | |
| 502 | + | d.in(ports.KeyEvent{Keysym: "l", Keycode: 38, Mods: ports.ModSuper}) | |
| 503 | + | d.settle(t) | |
| 504 | + | d.drain() | |
| 505 | + | d.in(ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper, Pressed: true}) | |
| 506 | + | d.in(ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper}) | |
| 507 | + | set := d.settle(t) | |
| 508 | + | wide := dragRect(t, set, dragOut.Name, 2) | |
| 509 | + | if wide.X == d.two.X { | |
| 510 | + | t.Fatalf("the column did not move: %v", wide) | |
| 511 | + | } | |
| 512 | + | x, y := float64(d.two.X+d.two.W-20-wide.X), float64(d.two.Y+30-wide.Y) | |
| 513 | + | got := dataDragCommands(d.drain()) | |
| 514 | + | if !slices.Contains(got, ports.ClientCommand(ports.DataDragMotion{ID: 2, X: x, Y: y, Time: 2 * time.Millisecond})) { | |
| 515 | + | t.Fatalf("no motion to %v,%v: %#v", x, y, got) | |
| 516 | + | } | |
| 517 | + | } | |
| 518 | + | ||
| 519 | + | func TestDataDragDropsOnItsOwnButton(t *testing.T) { | |
| 520 | + | d := startDnd(t) | |
| 521 | + | // Right pressed after left: the drag is left's, the grab's. | |
| 522 | + | d.in(ports.PointerButton{Button: 0x111, Pressed: true}) | |
| 523 | + | d.settle(t) | |
| 524 | + | d.drain() | |
| 525 | + | d.start(t, 0) | |
| 526 | + | d.in(ports.PointerButton{Button: 0x111}) | |
| 527 | + | d.settle(t) | |
| 528 | + | if got := dataDragCommands(d.drain()); slices.Contains(got, ports.ClientCommand(ports.DataDragDrop{})) { | |
| 529 | + | t.Fatalf("right's release dropped: %#v", got) | |
| 530 | + | } | |
| 531 | + | d.in(ports.PointerButton{Button: 0x110}) | |
| 532 | + | d.settle(t) | |
| 533 | + | if got := dataDragCommands(d.drain()); len(got) == 0 || got[0] != (ports.DataDragDrop{}) { | |
| 534 | + | t.Fatalf("left's release did not drop: %#v", got) | |
| 535 | + | } | |
| 536 | + | } | |
| 537 | + | ||
| 538 | + | func TestDataDragEscapeCancelsOverSwitcher(t *testing.T) { | |
| 539 | + | d := startDnd(t) | |
| 540 | + | d.start(t, 0) | |
| 541 | + | d.in(ports.KeyEvent{Keysym: "Super_L", Keycode: 125, Mods: ports.ModSuper, Pressed: true}) | |
| 542 | + | d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper, Pressed: true}) | |
| 543 | + | d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper}) | |
| 544 | + | d.settle(t) | |
| 545 | + | d.drain() | |
| 546 | + | d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1, Mods: ports.ModSuper, Pressed: true}) | |
| 547 | + | d.settle(t) | |
| 548 | + | got := dataDragCommands(d.drain()) | |
| 549 | + | if len(got) == 0 || got[0] != (ports.DataDragCancel{}) { | |
| 550 | + | t.Fatalf("Escape did not cancel the drag: %#v", got) | |
| 551 | + | } | |
| 552 | + | d.in(ports.PointerButton{Button: 0x110}) | |
| 553 | + | d.settle(t) | |
| 554 | + | if slices.Contains(dataDragCommands(d.drain()), ports.ClientCommand(ports.DataDragDrop{})) { | |
| 555 | + | t.Fatal("dropped after Escape") | |
| 556 | + | } | |
| 557 | + | } | |
| 558 | + | ||
| 559 | + | func TestDataDragIgnoresCompositorSwipes(t *testing.T) { | |
| 560 | + | d := startDnd(t) | |
| 561 | + | d.start(t, 0) | |
| 562 | + | before := d.settle(t) | |
| 563 | + | // Flicks that page columns and workspaces, as navigation would. | |
| 564 | + | for _, fingers := range []int{3, 4} { | |
| 565 | + | at := time.Second | |
| 566 | + | d.in(ports.SwipeBegin{Fingers: fingers, Time: at}) | |
| 567 | + | for range 10 { | |
| 568 | + | at += 8 * time.Millisecond | |
| 569 | + | d.in(ports.SwipeUpdate{DX: 60, AccelDX: 60, DY: 40, AccelDY: 40, Time: at}) | |
| 570 | + | } | |
| 571 | + | d.in(ports.SwipeEnd{Time: at}) | |
| 572 | + | } | |
| 573 | + | after := d.settle(t) | |
| 574 | + | if !slices.Equal(before[0].Windows, after[0].Windows) { | |
| 575 | + | t.Fatalf("a swipe moved windows during the drag:\n%#v\n%#v", before[0].Windows, after[0].Windows) | |
| 576 | + | } | |
| 577 | + | } | |
| @@ -0,0 +1,577 @@ | |||||
| 1 | + | package core_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | "testing" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | const dndIcon ports.WindowID = 900 | |||
| 12 | + | ||||
| 13 | + | // dndRig is a drag rig with tiles 1 and 2 side by side and the pointer | |||
| 14 | + | // on 1 with its button held, as a client that is about to start_drag. | |||
| 15 | + | type dndRig struct { | |||
| 16 | + | *dragRig | |||
| 17 | + | one, two ports.Rect | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | func startDnd(t *testing.T, outs ...ports.OutputInfo) *dndRig { | |||
| 21 | + | t.Helper() | |||
| 22 | + | if len(outs) == 0 { | |||
| 23 | + | outs = []ports.OutputInfo{dragOut} | |||
| 24 | + | } | |||
| 25 | + | r := startDragRig(t, nil, outs...) | |||
| 26 | + | r.client <- ports.WindowMapped{ID: 1} | |||
| 27 | + | r.client <- ports.WindowMapped{ID: 2} | |||
| 28 | + | set := r.settle(t) | |||
| 29 | + | d := &dndRig{dragRig: r, one: dragRect(t, set, outs[0].Name, 1), two: dragRect(t, set, outs[0].Name, 2)} | |||
| 30 | + | r.moveTo(float64(d.one.X+20), float64(d.one.Y+30), 1) | |||
| 31 | + | r.in(ports.PointerButton{Button: 0x110, Pressed: true}) | |||
| 32 | + | r.settle(t) | |||
| 33 | + | r.drain() | |||
| 34 | + | return d | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | // dataDragCommands keeps what the data drag sends and the pointer events | |||
| 38 | + | // no client may see during it. | |||
| 39 | + | func dataDragCommands(cmds []ports.ClientCommand) []ports.ClientCommand { | |||
| 40 | + | var out []ports.ClientCommand | |||
| 41 | + | for _, v := range cmds { | |||
| 42 | + | switch v.(type) { | |||
| 43 | + | case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel, | |||
| 44 | + | ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, | |||
| 45 | + | ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo: | |||
| 46 | + | out = append(out, v) | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | return out | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func (d *dndRig) start(t *testing.T, icon ports.WindowID) []ports.ClientCommand { | |||
| 53 | + | t.Helper() | |||
| 54 | + | d.client <- ports.DataDragStarted{Button: 0x110, Icon: icon} | |||
| 55 | + | d.settle(t) | |||
| 56 | + | return dataDragCommands(d.drain()) | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | func wantCommands(t *testing.T, got []ports.ClientCommand, want ...ports.ClientCommand) { | |||
| 60 | + | t.Helper() | |||
| 61 | + | if !slices.Equal(got, want) { | |||
| 62 | + | t.Fatalf("commands %#v, want %#v", got, want) | |||
| 63 | + | } | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | func TestDataDragRoutesToSurfaceUnderPointer(t *testing.T) { | |||
| 67 | + | d := startDnd(t) | |||
| 68 | + | // The origin loses the pointer and is the first target. | |||
| 69 | + | wantCommands(t, d.start(t, 0), | |||
| 70 | + | ports.PointerFocus{}, | |||
| 71 | + | ports.DataDragFocus{ID: 1, X: 20, Y: 30, Time: time.Millisecond}) | |||
| 72 | + | // Motion over the same surface. | |||
| 73 | + | d.moveTo(float64(d.one.X+25), float64(d.one.Y+35), 2) | |||
| 74 | + | d.settle(t) | |||
| 75 | + | wantCommands(t, dataDragCommands(d.drain()), ports.DataDragMotion{ID: 1, X: 25, Y: 35, Time: 2 * time.Millisecond}) | |||
| 76 | + | // Over the other window: it is the new target. | |||
| 77 | + | d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3) | |||
| 78 | + | d.settle(t) | |||
| 79 | + | wantCommands(t, dataDragCommands(d.drain()), ports.DataDragFocus{ID: 2, X: 5, Y: 6, Time: 3 * time.Millisecond}) | |||
| 80 | + | d.moveTo(float64(d.two.X+7), float64(d.two.Y+8), 4) | |||
| 81 | + | d.settle(t) | |||
| 82 | + | wantCommands(t, dataDragCommands(d.drain()), ports.DataDragMotion{ID: 2, X: 7, Y: 8, Time: 4 * time.Millisecond}) | |||
| 83 | + | // Over nothing: no target. | |||
| 84 | + | d.moveTo(float64(d.two.X+d.two.W+5), float64(d.two.Y+8), 5) | |||
| 85 | + | d.settle(t) | |||
| 86 | + | wantCommands(t, dataDragCommands(d.drain()), ports.DataDragFocus{Time: 5 * time.Millisecond}) | |||
| 87 | + | d.moveTo(float64(d.two.X+d.two.W+6), float64(d.two.Y+8), 6) | |||
| 88 | + | d.settle(t) | |||
| 89 | + | wantCommands(t, dataDragCommands(d.drain())) | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | func TestDataDragDropsOnRelease(t *testing.T) { | |||
| 93 | + | d := startDnd(t) | |||
| 94 | + | d.start(t, 0) | |||
| 95 | + | d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3) | |||
| 96 | + | d.in(ports.PointerButton{Button: 0x110, Time: 4 * time.Millisecond}) | |||
| 97 | + | d.settle(t) | |||
| 98 | + | // The release is the drop; the pointer then points at the window under | |||
| 99 | + | // the cursor, not at the origin that grabbed it. | |||
| 100 | + | wantCommands(t, dataDragCommands(d.drain()), | |||
| 101 | + | ports.DataDragFocus{ID: 2, X: 5, Y: 6, Time: 3 * time.Millisecond}, | |||
| 102 | + | ports.DataDragDrop{}, | |||
| 103 | + | ports.PointerFocus{ID: 2, X: 5, Y: 6}) | |||
| 104 | + | // The drag is over: motion is plain pointer motion again. | |||
| 105 | + | d.moveTo(float64(d.two.X+9), float64(d.two.Y+9), 5) | |||
| 106 | + | d.settle(t) | |||
| 107 | + | wantCommands(t, dataDragCommands(d.drain()), ports.PointerMotionTo{ID: 2, X: 9, Y: 9, Time: 5 * time.Millisecond}) | |||
| 108 | + | // The next press is an ordinary click. | |||
| 109 | + | d.in(ports.PointerButton{Button: 0x110, Pressed: true}) | |||
| 110 | + | d.settle(t) | |||
| 111 | + | wantCommands(t, dataDragCommands(d.drain()), ports.PointerButtonTo{ID: 2, Button: 0x110, Pressed: true, Grab: true}) | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | func TestDataDragSendsNothingToClients(t *testing.T) { | |||
| 115 | + | d := startDnd(t) | |||
| 116 | + | d.start(t, 0) | |||
| 117 | + | d.in(ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 1}}) | |||
| 118 | + | d.in(ports.PinchBegin{Fingers: 2, Time: time.Second}) | |||
| 119 | + | d.in(ports.PinchUpdate{Scale: 2, Time: time.Second}) | |||
| 120 | + | d.in(ports.PinchEnd{Time: time.Second}) | |||
| 121 | + | d.in(ports.HoldBegin{Fingers: 2, Time: time.Second}) | |||
| 122 | + | d.in(ports.HoldEnd{Time: time.Second}) | |||
| 123 | + | d.in(ports.SwipeBegin{Fingers: 2, Time: time.Second}) | |||
| 124 | + | d.in(ports.SwipeEnd{Time: time.Second}) | |||
| 125 | + | // Other buttons neither press nor release anywhere. | |||
| 126 | + | d.in(ports.PointerButton{Button: 0x111, Pressed: true}) | |||
| 127 | + | d.in(ports.PointerButton{Button: 0x111}) | |||
| 128 | + | d.settle(t) | |||
| 129 | + | for _, v := range dataDragCommands(d.drain()) { | |||
| 130 | + | switch v.(type) { | |||
| 131 | + | case ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.PointerMotionTo, ports.DataDragDrop, ports.DataDragCancel: | |||
| 132 | + | t.Fatalf("sent %#v", v) | |||
| 133 | + | } | |||
| 134 | + | } | |||
| 135 | + | // Still dragging: the drag button's release drops. | |||
| 136 | + | d.in(ports.PointerButton{Button: 0x110}) | |||
| 137 | + | d.settle(t) | |||
| 138 | + | if got := dataDragCommands(d.drain()); len(got) == 0 || got[0] != (ports.DataDragDrop{}) { | |||
| 139 | + | t.Fatalf("commands %#v", got) | |||
| 140 | + | } | |||
| 141 | + | } | |||
| 142 | + | ||||
| 143 | + | func TestDataDragKeyboardKeepsForwarding(t *testing.T) { | |||
| 144 | + | d := startDnd(t) | |||
| 145 | + | d.start(t, 0) | |||
| 146 | + | d.in(ports.KeyEvent{Keysym: "Shift_L", Keycode: 42, Pressed: true, Mods: ports.ModShift}) | |||
| 147 | + | d.in(ports.KeyEvent{Keysym: "Shift_L", Keycode: 42}) | |||
| 148 | + | d.settle(t) | |||
| 149 | + | n := 0 | |||
| 150 | + | for _, v := range d.drain() { | |||
| 151 | + | if _, ok := v.(ports.ForwardKey); ok { | |||
| 152 | + | n++ | |||
| 153 | + | } | |||
| 154 | + | } | |||
| 155 | + | if n != 2 { | |||
| 156 | + | t.Fatalf("%d keys forwarded, want 2", n) | |||
| 157 | + | } | |||
| 158 | + | } | |||
| 159 | + | ||||
| 160 | + | func TestDataDragEscapeCancels(t *testing.T) { | |||
| 161 | + | d := startDnd(t) | |||
| 162 | + | d.start(t, 0) | |||
| 163 | + | d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3) | |||
| 164 | + | d.settle(t) | |||
| 165 | + | d.drain() | |||
| 166 | + | d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1, Pressed: true}) | |||
| 167 | + | d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1}) | |||
| 168 | + | d.in(ports.PointerButton{Button: 0x110}) | |||
| 169 | + | d.settle(t) | |||
| 170 | + | cmds := d.drain() | |||
| 171 | + | wantCommands(t, dataDragCommands(cmds), | |||
| 172 | + | ports.DataDragCancel{}, | |||
| 173 | + | ports.PointerFocus{ID: 2, X: 5, Y: 6}) | |||
| 174 | + | for _, v := range cmds { | |||
| 175 | + | if k, ok := v.(ports.ForwardKey); ok { | |||
| 176 | + | t.Fatalf("client saw %#v", k) | |||
| 177 | + | } | |||
| 178 | + | } | |||
| 179 | + | // The press that started the drag is over. | |||
| 180 | + | d.moveTo(float64(d.two.X+9), float64(d.two.Y+9), 5) | |||
| 181 | + | d.settle(t) | |||
| 182 | + | wantCommands(t, dataDragCommands(d.drain()), ports.PointerMotionTo{ID: 2, X: 9, Y: 9, Time: 5 * time.Millisecond}) | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | func TestDataDragEndedByWayland(t *testing.T) { | |||
| 186 | + | d := startDnd(t) | |||
| 187 | + | d.start(t, dndIcon) | |||
| 188 | + | d.client <- ports.DataDragIcon{ID: dndIcon, X: 1, Y: 2, W: 16, H: 16} | |||
| 189 | + | d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3) | |||
| 190 | + | d.settle(t) | |||
| 191 | + | d.drain() | |||
| 192 | + | // The source died: no command, the icon goes and the pointer is | |||
| 193 | + | // pointed at the window under the cursor. | |||
| 194 | + | d.client <- ports.DataDragEnded{} | |||
| 195 | + | set := d.settle(t) | |||
| 196 | + | if _, ok := windowIn(set, "A", dndIcon); ok { | |||
| 197 | + | t.Fatal("icon still drawn") | |||
| 198 | + | } | |||
| 199 | + | wantCommands(t, dataDragCommands(d.drain()), | |||
| 200 | + | ports.PointerFocus{ID: 2, X: 5, Y: 6}) | |||
| 201 | + | // The held button's release reaches nobody. | |||
| 202 | + | d.in(ports.PointerButton{Button: 0x110}) | |||
| 203 | + | d.settle(t) | |||
| 204 | + | wantCommands(t, dataDragCommands(d.drain())) | |||
| 205 | + | // An end with no drag is ignored. | |||
| 206 | + | d.client <- ports.DataDragEnded{} | |||
| 207 | + | d.settle(t) | |||
| 208 | + | wantCommands(t, dataDragCommands(d.drain())) | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | func TestDataDragRefused(t *testing.T) { | |||
| 212 | + | t.Run("no press", func(t *testing.T) { | |||
| 213 | + | d := startDnd(t) | |||
| 214 | + | d.in(ports.PointerButton{Button: 0x110}) | |||
| 215 | + | d.settle(t) | |||
| 216 | + | d.drain() | |||
| 217 | + | wantCommands(t, d.start(t, 0), ports.DataDragCancel{}) | |||
| 218 | + | // No drag started: motion is plain. | |||
| 219 | + | d.moveTo(float64(d.one.X+21), float64(d.one.Y+30), 2) | |||
| 220 | + | d.settle(t) | |||
| 221 | + | wantCommands(t, dataDragCommands(d.drain()), ports.PointerMotionTo{ID: 1, X: 21, Y: 30, Time: 2 * time.Millisecond}) | |||
| 222 | + | }) | |||
| 223 | + | t.Run("window drag", func(t *testing.T) { | |||
| 224 | + | r := startDragRig(t, nil, dragOut) | |||
| 225 | + | r.client <- ports.WindowMapped{ID: 1} | |||
| 226 | + | r.client <- ports.WindowMapped{ID: 2} | |||
| 227 | + | set := r.settle(t) | |||
| 228 | + | one := dragRect(t, set, "A", 1) | |||
| 229 | + | r.moveTo(float64(one.X+20), float64(one.Y+30), 1) | |||
| 230 | + | r.settle(t) | |||
| 231 | + | r.press(t, 0x110, ports.ModSuper) | |||
| 232 | + | r.settle(t) | |||
| 233 | + | r.drain() | |||
| 234 | + | r.client <- ports.DataDragStarted{Button: 0x110} | |||
| 235 | + | r.settle(t) | |||
| 236 | + | wantCommands(t, r.drain(), ports.DataDragCancel{}) | |||
| 237 | + | // The window drag goes on. | |||
| 238 | + | r.moveTo(float64(one.X+200), float64(one.Y+30), 2) | |||
| 239 | + | r.in(ports.PointerButton{Button: 0x110}) | |||
| 240 | + | r.settle(t) | |||
| 241 | + | wantCommands(t, dataDragCommands(r.drain()), ports.PointerFocus{ID: 1, X: 200, Y: 30}) | |||
| 242 | + | }) | |||
| 243 | + | t.Run("twice", func(t *testing.T) { | |||
| 244 | + | d := startDnd(t) | |||
| 245 | + | d.start(t, 0) | |||
| 246 | + | wantCommands(t, d.start(t, 0), ports.DataDragCancel{}) | |||
| 247 | + | }) | |||
| 248 | + | t.Run("locked", func(t *testing.T) { | |||
| 249 | + | d := startDnd(t) | |||
| 250 | + | locked := ports.SecurityState{Generation: 1, Protected: true} | |||
| 251 | + | d.epoch.Store(locked) | |||
| 252 | + | d.client <- ports.SessionLockChanged{State: locked} | |||
| 253 | + | d.settle(t) | |||
| 254 | + | d.drain() | |||
| 255 | + | d.client <- ports.DataDragStarted{Button: 0x110} | |||
| 256 | + | d.settle(t) | |||
| 257 | + | var got []ports.ClientCommand | |||
| 258 | + | for _, v := range d.drain() { | |||
| 259 | + | switch v.(type) { | |||
| 260 | + | case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel: | |||
| 261 | + | got = append(got, v) | |||
| 262 | + | } | |||
| 263 | + | } | |||
| 264 | + | wantCommands(t, got, ports.DataDragCancel{}) | |||
| 265 | + | }) | |||
| 266 | + | } | |||
| 267 | + | ||||
| 268 | + | func TestDataDragCommandsAreSecurityWrapped(t *testing.T) { | |||
| 269 | + | d := startDnd(t) | |||
| 270 | + | d.client <- ports.DataDragStarted{Button: 0x110} | |||
| 271 | + | d.settle(t) | |||
| 272 | + | n := 0 | |||
| 273 | + | for { | |||
| 274 | + | select { | |||
| 275 | + | case v := <-d.commands: | |||
| 276 | + | s, ok := v.(ports.SecurityCommand) | |||
| 277 | + | if !ok { | |||
| 278 | + | t.Fatalf("unwrapped %#v", v) | |||
| 279 | + | } | |||
| 280 | + | if _, ok := s.Command.(ports.DataDragFocus); ok { | |||
| 281 | + | n++ | |||
| 282 | + | } | |||
| 283 | + | continue | |||
| 284 | + | default: | |||
| 285 | + | } | |||
| 286 | + | break | |||
| 287 | + | } | |||
| 288 | + | if n != 1 { | |||
| 289 | + | t.Fatalf("%d DataDragFocus", n) | |||
| 290 | + | } | |||
| 291 | + | } | |||
| 292 | + | ||||
| 293 | + | func TestDataDragLockCancelsIt(t *testing.T) { | |||
| 294 | + | d := startDnd(t) | |||
| 295 | + | d.start(t, dndIcon) | |||
| 296 | + | d.client <- ports.DataDragIcon{ID: dndIcon, W: 16, H: 16} | |||
| 297 | + | d.settle(t) | |||
| 298 | + | locked := ports.SecurityState{Generation: 1, Protected: true} | |||
| 299 | + | d.epoch.Store(locked) | |||
| 300 | + | d.client <- ports.SessionLockChanged{State: locked, Surfaces: []ports.LockSurfacePlacement{{ID: 10, Output: "A", Width: 1000, Height: 600}}} | |||
| 301 | + | set := d.settle(t) | |||
| 302 | + | if _, ok := windowIn(set, "A", dndIcon); ok { | |||
| 303 | + | t.Fatal("icon drawn while locked") | |||
| 304 | + | } | |||
| 305 | + | d.drain() | |||
| 306 | + | d.in(ports.PointerMotion{X: 100, Y: 100, Time: time.Second}) | |||
| 307 | + | d.settle(t) | |||
| 308 | + | for _, v := range d.drain() { | |||
| 309 | + | switch v.(type) { | |||
| 310 | + | case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop: | |||
| 311 | + | t.Fatalf("data drag alive while locked: %#v", v) | |||
| 312 | + | } | |||
| 313 | + | } | |||
| 314 | + | // After the unlock, the old drag is gone: plain pointer events. | |||
| 315 | + | unlocked := ports.SecurityState{Generation: 2} | |||
| 316 | + | d.epoch.Store(unlocked) | |||
| 317 | + | d.client <- ports.SessionLockChanged{State: unlocked} | |||
| 318 | + | d.settle(t) | |||
| 319 | + | d.drain() | |||
| 320 | + | d.moveTo(float64(d.one.X+21), float64(d.one.Y+30), 9) | |||
| 321 | + | d.settle(t) | |||
| 322 | + | got := dataDragCommands(d.drain()) | |||
| 323 | + | wantCommands(t, got, ports.PointerFocus{ID: 1, X: 21, Y: 30}, ports.PointerMotionTo{ID: 1, X: 21, Y: 30, Time: 9 * time.Millisecond}) | |||
| 324 | + | } | |||
| 325 | + | ||||
| 326 | + | func TestDataDragTargetUnmapped(t *testing.T) { | |||
| 327 | + | d := startDnd(t) | |||
| 328 | + | d.start(t, 0) | |||
| 329 | + | d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3) | |||
| 330 | + | d.settle(t) | |||
| 331 | + | d.drain() | |||
| 332 | + | d.client <- ports.WindowUnmapped{ID: 2} | |||
| 333 | + | d.settle(t) | |||
| 334 | + | cmds := dataDragCommands(d.drain()) | |||
| 335 | + | if len(cmds) == 0 || cmds[0] != (ports.DataDragFocus{Time: 3 * time.Millisecond}) { | |||
| 336 | + | t.Fatalf("commands %#v", cmds) | |||
| 337 | + | } | |||
| 338 | + | } | |||
| 339 | + | ||||
| 340 | + | func TestDataDragIconIsLastWindowAtCursor(t *testing.T) { | |||
| 341 | + | d := startDnd(t) | |||
| 342 | + | d.start(t, dndIcon) | |||
| 343 | + | // Not drawn until wayland measured it. | |||
| 344 | + | if _, ok := windowIn(d.settle(t), "A", dndIcon); ok { | |||
| 345 | + | t.Fatal("icon drawn before its size") | |||
| 346 | + | } | |||
| 347 | + | cx, cy := d.one.X+20, d.one.Y+30 | |||
| 348 | + | d.client <- ports.DataDragIcon{ID: dndIcon, X: -4, Y: -6, W: 32, H: 24} | |||
| 349 | + | set := d.settle(t) | |||
| 350 | + | s := dragScene(set, "A") | |||
| 351 | + | last := s.Windows[len(s.Windows)-1] | |||
| 352 | + | want := ports.SceneWindow{ID: dndIcon, Rect: ports.Rect{X: cx - 4, Y: cy - 6, W: 32, H: 24}, Popup: true, OverLayers: true} | |||
| 353 | + | if last != want { | |||
| 354 | + | t.Fatalf("last window %+v, want %+v", last, want) | |||
| 355 | + | } | |||
| 356 | + | // It follows the cursor; the scene changes. | |||
| 357 | + | d.moveTo(float64(cx+100), float64(cy+50), 2) | |||
| 358 | + | moved := dragScene(d.settle(t), "A") | |||
| 359 | + | if moved.SameAs(s) || moved.Seq == s.Seq { | |||
| 360 | + | t.Fatal("scene did not change with the icon") | |||
| 361 | + | } | |||
| 362 | + | if got := moved.Windows[len(moved.Windows)-1]; got.Rect.X != cx+96 || got.Rect.Y != cy+44 || got.ID != dndIcon { | |||
| 363 | + | t.Fatalf("moved icon %+v", got) | |||
| 364 | + | } | |||
| 365 | + | // Under the icon, the surface below is still the target. | |||
| 366 | + | d.drain() | |||
| 367 | + | d.moveTo(float64(cx+101), float64(cy+50), 3) | |||
| 368 | + | d.settle(t) | |||
| 369 | + | for _, v := range d.drain() { | |||
| 370 | + | if f, ok := v.(ports.DataDragFocus); ok && f.ID == dndIcon { | |||
| 371 | + | t.Fatalf("icon was targeted: %#v", f) | |||
| 372 | + | } | |||
| 373 | + | } | |||
| 374 | + | // A commit that changes the offset moves it; a zero size hides it. | |||
| 375 | + | d.client <- ports.DataDragIcon{ID: dndIcon, X: 0, Y: 0, W: 32, H: 24} | |||
| 376 | + | if got := dragScene(d.settle(t), "A"); got.Windows[len(got.Windows)-1].Rect.X != cx+101 { | |||
| 377 | + | t.Fatalf("offset ignored: %+v", got.Windows[len(got.Windows)-1]) | |||
| 378 | + | } | |||
| 379 | + | d.client <- ports.DataDragIcon{ID: dndIcon} | |||
| 380 | + | if _, ok := windowIn(d.settle(t), "A", dndIcon); ok { | |||
| 381 | + | t.Fatal("hidden icon drawn") | |||
| 382 | + | } | |||
| 383 | + | // Geometry of another surface is not the icon's. | |||
| 384 | + | d.client <- ports.DataDragIcon{ID: dndIcon + 1, W: 10, H: 10} | |||
| 385 | + | if _, ok := windowIn(d.settle(t), "A", dndIcon+1); ok { | |||
| 386 | + | t.Fatal("foreign icon drawn") | |||
| 387 | + | } | |||
| 388 | + | // The drop removes it. | |||
| 389 | + | d.client <- ports.DataDragIcon{ID: dndIcon, W: 32, H: 24} | |||
| 390 | + | d.settle(t) | |||
| 391 | + | d.in(ports.PointerButton{Button: 0x110}) | |||
| 392 | + | set = d.settle(t) | |||
| 393 | + | if _, ok := windowIn(set, "A", dndIcon); ok { | |||
| 394 | + | t.Fatal("icon drawn after the drop") | |||
| 395 | + | } | |||
| 396 | + | } | |||
| 397 | + | ||||
| 398 | + | func TestDataDragIconOnScreenUnderCursor(t *testing.T) { | |||
| 399 | + | other := ports.OutputInfo{Name: "B", Width: 800, Height: 600} | |||
| 400 | + | d := startDnd(t, dragOut, other) | |||
| 401 | + | d.start(t, dndIcon) | |||
| 402 | + | d.client <- ports.DataDragIcon{ID: dndIcon, X: 2, Y: 3, W: 10, H: 10} | |||
| 403 | + | d.moveTo(1000+100, 50, 2) | |||
| 404 | + | set := d.settle(t) | |||
| 405 | + | if _, ok := windowIn(set, "A", dndIcon); ok { | |||
| 406 | + | t.Fatal("icon on the other screen") | |||
| 407 | + | } | |||
| 408 | + | w, ok := windowIn(set, "B", dndIcon) | |||
| 409 | + | if !ok { | |||
| 410 | + | t.Fatal("no icon on B") | |||
| 411 | + | } | |||
| 412 | + | if w.Rect != (ports.Rect{X: 102, Y: 53, W: 10, H: 10}) { | |||
| 413 | + | t.Fatalf("icon %+v", w.Rect) | |||
| 414 | + | } | |||
| 415 | + | } | |||
| 416 | + | ||||
| 417 | + | func TestDataDragNoIconIgnoresIconEvents(t *testing.T) { | |||
| 418 | + | d := startDnd(t) | |||
| 419 | + | d.start(t, 0) | |||
| 420 | + | d.client <- ports.DataDragIcon{ID: dndIcon, W: 10, H: 10} | |||
| 421 | + | if _, ok := windowIn(d.settle(t), "A", dndIcon); ok { | |||
| 422 | + | t.Fatal("icon drawn for a drag without one") | |||
| 423 | + | } | |||
| 424 | + | // Nor with no drag at all. | |||
| 425 | + | d.in(ports.PointerButton{Button: 0x110}) | |||
| 426 | + | d.settle(t) | |||
| 427 | + | d.client <- ports.DataDragIcon{ID: dndIcon, W: 10, H: 10} | |||
| 428 | + | if _, ok := windowIn(d.settle(t), "A", dndIcon); ok { | |||
| 429 | + | t.Fatal("icon drawn without a drag") | |||
| 430 | + | } | |||
| 431 | + | } | |||
| 432 | + | ||||
| 433 | + | func TestDataDragFocusScreenFollowsPointer(t *testing.T) { | |||
| 434 | + | r := startDragRig(t, nil, dragOut, ports.OutputInfo{Name: "B", Width: 1000, Height: 600}) | |||
| 435 | + | r.client <- ports.WindowMapped{ID: 1} | |||
| 436 | + | r.settle(t) | |||
| 437 | + | r.moveTo(1500, 300, 1) | |||
| 438 | + | r.settle(t) | |||
| 439 | + | r.client <- ports.WindowMapped{ID: 3} | |||
| 440 | + | set := r.settle(t) | |||
| 441 | + | one := dragRect(t, set, "A", 1) | |||
| 442 | + | r.moveTo(float64(one.X+20), float64(one.Y+30), 2) | |||
| 443 | + | set = r.settle(t) | |||
| 444 | + | if w, _ := windowIn(set, "A", 1); !w.Focused { | |||
| 445 | + | t.Fatal("1 not focused") | |||
| 446 | + | } | |||
| 447 | + | r.in(ports.PointerButton{Button: 0x110, Pressed: true}) | |||
| 448 | + | r.settle(t) | |||
| 449 | + | r.client <- ports.DataDragStarted{Button: 0x110} | |||
| 450 | + | r.moveTo(1500, 300, 3) | |||
| 451 | + | set = r.settle(t) | |||
| 452 | + | if w, _ := windowIn(set, "B", 3); !w.Focused { | |||
| 453 | + | t.Fatal("the screen under the drag did not take the focus") | |||
| 454 | + | } | |||
| 455 | + | three := dragRect(t, set, "B", 3) | |||
| 456 | + | var focus []ports.DataDragFocus | |||
| 457 | + | for _, v := range r.drain() { | |||
| 458 | + | if f, ok := v.(ports.DataDragFocus); ok { | |||
| 459 | + | focus = append(focus, f) | |||
| 460 | + | } | |||
| 461 | + | } | |||
| 462 | + | if n := len(focus); n != 2 || focus[1].ID != 3 || focus[1].X != float64(1500-1000-three.X) { | |||
| 463 | + | t.Fatalf("focus %+v", focus) | |||
| 464 | + | } | |||
| 465 | + | } | |||
| 466 | + | ||||
| 467 | + | func TestDataDragFollowsWorkspaceSwitchUnderStillCursor(t *testing.T) { | |||
| 468 | + | d := startDnd(t) | |||
| 469 | + | d.start(t, 0) | |||
| 470 | + | d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3) | |||
| 471 | + | d.settle(t) | |||
| 472 | + | d.drain() | |||
| 473 | + | // cmd+2 shows an empty workspace under the still cursor: the target | |||
| 474 | + | // leaves, and the release drops on nothing. | |||
| 475 | + | d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper, Pressed: true}) | |||
| 476 | + | d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper}) | |||
| 477 | + | d.settle(t) | |||
| 478 | + | got := dataDragCommands(d.drain()) | |||
| 479 | + | if !slices.Contains(got, ports.ClientCommand(ports.DataDragFocus{Time: 3 * time.Millisecond})) { | |||
| 480 | + | t.Fatalf("no leave after the switch: %#v", got) | |||
| 481 | + | } | |||
| 482 | + | d.in(ports.PointerButton{Button: 0x110, Time: 4 * time.Millisecond}) | |||
| 483 | + | d.settle(t) | |||
| 484 | + | got = dataDragCommands(d.drain()) | |||
| 485 | + | if len(got) == 0 || got[0] != (ports.DataDragDrop{}) { | |||
| 486 | + | t.Fatalf("commands %#v", got) | |||
| 487 | + | } | |||
| 488 | + | for _, v := range got { | |||
| 489 | + | if f, ok := v.(ports.DataDragFocus); ok && f.ID != 0 { | |||
| 490 | + | t.Fatalf("hidden window targeted: %#v", got) | |||
| 491 | + | } | |||
| 492 | + | } | |||
| 493 | + | } | |||
| 494 | + | ||||
| 495 | + | func TestDataDragTargetMovedUnderStillCursor(t *testing.T) { | |||
| 496 | + | d := startDnd(t) | |||
| 497 | + | d.start(t, 0) | |||
| 498 | + | // Over 2, focused by a click-free keyboard move, then cmd+f maximizes | |||
| 499 | + | // its column: still the target under the cursor, at another point. | |||
| 500 | + | d.moveTo(float64(d.two.X+d.two.W-20), float64(d.two.Y+30), 2) | |||
| 501 | + | d.in(ports.KeyEvent{Keysym: "l", Keycode: 38, Mods: ports.ModSuper, Pressed: true}) | |||
| 502 | + | d.in(ports.KeyEvent{Keysym: "l", Keycode: 38, Mods: ports.ModSuper}) | |||
| 503 | + | d.settle(t) | |||
| 504 | + | d.drain() | |||
| 505 | + | d.in(ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper, Pressed: true}) | |||
| 506 | + | d.in(ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper}) | |||
| 507 | + | set := d.settle(t) | |||
| 508 | + | wide := dragRect(t, set, dragOut.Name, 2) | |||
| 509 | + | if wide.X == d.two.X { | |||
| 510 | + | t.Fatalf("the column did not move: %v", wide) | |||
| 511 | + | } | |||
| 512 | + | x, y := float64(d.two.X+d.two.W-20-wide.X), float64(d.two.Y+30-wide.Y) | |||
| 513 | + | got := dataDragCommands(d.drain()) | |||
| 514 | + | if !slices.Contains(got, ports.ClientCommand(ports.DataDragMotion{ID: 2, X: x, Y: y, Time: 2 * time.Millisecond})) { | |||
| 515 | + | t.Fatalf("no motion to %v,%v: %#v", x, y, got) | |||
| 516 | + | } | |||
| 517 | + | } | |||
| 518 | + | ||||
| 519 | + | func TestDataDragDropsOnItsOwnButton(t *testing.T) { | |||
| 520 | + | d := startDnd(t) | |||
| 521 | + | // Right pressed after left: the drag is left's, the grab's. | |||
| 522 | + | d.in(ports.PointerButton{Button: 0x111, Pressed: true}) | |||
| 523 | + | d.settle(t) | |||
| 524 | + | d.drain() | |||
| 525 | + | d.start(t, 0) | |||
| 526 | + | d.in(ports.PointerButton{Button: 0x111}) | |||
| 527 | + | d.settle(t) | |||
| 528 | + | if got := dataDragCommands(d.drain()); slices.Contains(got, ports.ClientCommand(ports.DataDragDrop{})) { | |||
| 529 | + | t.Fatalf("right's release dropped: %#v", got) | |||
| 530 | + | } | |||
| 531 | + | d.in(ports.PointerButton{Button: 0x110}) | |||
| 532 | + | d.settle(t) | |||
| 533 | + | if got := dataDragCommands(d.drain()); len(got) == 0 || got[0] != (ports.DataDragDrop{}) { | |||
| 534 | + | t.Fatalf("left's release did not drop: %#v", got) | |||
| 535 | + | } | |||
| 536 | + | } | |||
| 537 | + | ||||
| 538 | + | func TestDataDragEscapeCancelsOverSwitcher(t *testing.T) { | |||
| 539 | + | d := startDnd(t) | |||
| 540 | + | d.start(t, 0) | |||
| 541 | + | d.in(ports.KeyEvent{Keysym: "Super_L", Keycode: 125, Mods: ports.ModSuper, Pressed: true}) | |||
| 542 | + | d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper, Pressed: true}) | |||
| 543 | + | d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper}) | |||
| 544 | + | d.settle(t) | |||
| 545 | + | d.drain() | |||
| 546 | + | d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1, Mods: ports.ModSuper, Pressed: true}) | |||
| 547 | + | d.settle(t) | |||
| 548 | + | got := dataDragCommands(d.drain()) | |||
| 549 | + | if len(got) == 0 || got[0] != (ports.DataDragCancel{}) { | |||
| 550 | + | t.Fatalf("Escape did not cancel the drag: %#v", got) | |||
| 551 | + | } | |||
| 552 | + | d.in(ports.PointerButton{Button: 0x110}) | |||
| 553 | + | d.settle(t) | |||
| 554 | + | if slices.Contains(dataDragCommands(d.drain()), ports.ClientCommand(ports.DataDragDrop{})) { | |||
| 555 | + | t.Fatal("dropped after Escape") | |||
| 556 | + | } | |||
| 557 | + | } | |||
| 558 | + | ||||
| 559 | + | func TestDataDragIgnoresCompositorSwipes(t *testing.T) { | |||
| 560 | + | d := startDnd(t) | |||
| 561 | + | d.start(t, 0) | |||
| 562 | + | before := d.settle(t) | |||
| 563 | + | // Flicks that page columns and workspaces, as navigation would. | |||
| 564 | + | for _, fingers := range []int{3, 4} { | |||
| 565 | + | at := time.Second | |||
| 566 | + | d.in(ports.SwipeBegin{Fingers: fingers, Time: at}) | |||
| 567 | + | for range 10 { | |||
| 568 | + | at += 8 * time.Millisecond | |||
| 569 | + | d.in(ports.SwipeUpdate{DX: 60, AccelDX: 60, DY: 40, AccelDY: 40, Time: at}) | |||
| 570 | + | } | |||
| 571 | + | d.in(ports.SwipeEnd{Time: at}) | |||
| 572 | + | } | |||
| 573 | + | after := d.settle(t) | |||
| 574 | + | if !slices.Equal(before[0].Windows, after[0].Windows) { | |||
| 575 | + | t.Fatalf("a swipe moved windows during the drag:\n%#v\n%#v", before[0].Windows, after[0].Windows) | |||
| 576 | + | } | |||
| 577 | + | } | |||
M internal/core/input.go +30 -4
| @@ -26,6 +26,10 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 26 | 26 | case ports.PointerAxis: | |
| 27 | 27 | return c.pointerAxis(ctx, v) | |
| 28 | 28 | case ports.SwipeBegin: | |
| 29 | + | if c.dataDrag != nil && !forwardsSwipe(v.Fingers) { | |
| 30 | + | // The drag owns the pointer: no navigation under it. | |
| 31 | + | return nil | |
| 32 | + | } | |
| 29 | 33 | if forwardsSwipe(v.Fingers) { | |
| 30 | 34 | return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time) | |
| 31 | 35 | } | |
| @@ -98,6 +102,11 @@ func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error { | |||
| 98 | 102 | // The dragged window and the others get no pointer events. | |
| 99 | 103 | return c.dragMotion(ctx, v.Time) | |
| 100 | 104 | } | |
| 105 | + | if c.dataDrag != nil { | |
| 106 | + | // A data drag goes to the surface under the pointer, by its own | |
| 107 | + | // commands. | |
| 108 | + | return c.dataDragMotion(ctx, v.Time) | |
| 109 | + | } | |
| 101 | 110 | id, x, y := c.hit(c.cursorX, c.cursorY) | |
| 102 | 111 | // Layout changes are intentionally re-hit-tested only on motion. | |
| 103 | 112 | if id != c.pointer { | |
| @@ -123,6 +132,9 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error { | |||
| 123 | 132 | delete(c.buttons, v.Button) | |
| 124 | 133 | return nil | |
| 125 | 134 | } | |
| 135 | + | if c.dataDrag != nil { | |
| 136 | + | return c.dataDragButton(ctx, v) | |
| 137 | + | } | |
| 126 | 138 | if d := c.drag; d != nil { | |
| 127 | 139 | if !v.Pressed && v.Button == d.button { | |
| 128 | 140 | delete(c.buttons, v.Button) | |
| @@ -160,8 +172,9 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error { | |||
| 160 | 172 | return err | |
| 161 | 173 | } | |
| 162 | 174 | } | |
| 175 | + | grab := v.Pressed && c.held() == 0 | |
| 163 | 176 | if v.Pressed { | |
| 164 | - | if c.held() == 0 { | |
| 177 | + | if grab { | |
| 165 | 178 | c.grab = id | |
| 166 | 179 | } | |
| 167 | 180 | c.buttons[v.Button] = true | |
| @@ -170,7 +183,7 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error { | |||
| 170 | 183 | delete(c.buttons, v.Button) | |
| 171 | 184 | } | |
| 172 | 185 | if id != 0 { | |
| 173 | - | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil { | |
| 186 | + | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Grab: grab, Time: v.Time}); err != nil { | |
| 174 | 187 | return err | |
| 175 | 188 | } | |
| 176 | 189 | // A click on an on-demand layer gives it the keyboard. | |
| @@ -226,7 +239,7 @@ func (c *Core) pointerAxis(ctx context.Context, v ports.PointerAxis) error { | |||
| 226 | 239 | } | |
| 227 | 240 | // Scroll goes to the window under the pointer, which has the | |
| 228 | 241 | // pointer focus even mid-drag. None during a window drag. | |
| 229 | - | if c.pointer != 0 && c.drag == nil { | |
| 242 | + | if c.pointer != 0 && c.drag == nil && c.dataDrag == nil { | |
| 230 | 243 | if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil { | |
| 231 | 244 | return err | |
| 232 | 245 | } | |
| @@ -244,6 +257,12 @@ func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error { | |||
| 244 | 257 | return err | |
| 245 | 258 | } | |
| 246 | 259 | } | |
| 260 | + | if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" { | |
| 261 | + | // Escape cancels the data drag first, even over the switcher; | |
| 262 | + | // its release goes nowhere either. | |
| 263 | + | c.pressed[heldKey(key)] = true | |
| 264 | + | return c.cancelDataDrag(ctx) | |
| 265 | + | } | |
| 247 | 266 | if c.switching() && key.Pressed && key.Keysym == "Escape" { | |
| 248 | 267 | // Escape cancels the switcher; its release goes nowhere either. | |
| 249 | 268 | c.pressed[heldKey(key)] = true | |
| @@ -372,7 +391,14 @@ func (c *Core) runBind(ctx context.Context, action Action) error { | |||
| 372 | 391 | return err | |
| 373 | 392 | } | |
| 374 | 393 | } | |
| 375 | - | return c.publish(ctx) | |
| 394 | + | if err := c.publish(ctx); err != nil { | |
| 395 | + | return err | |
| 396 | + | } | |
| 397 | + | if c.dataDrag != nil { | |
| 398 | + | // A data drag follows the layout under the still cursor. | |
| 399 | + | return c.rehit(ctx) | |
| 400 | + | } | |
| 401 | + | return nil | |
| 376 | 402 | } | |
| 377 | 403 | ||
| 378 | 404 | // runSpawn starts the programs of a spawn effect of an action applied to w: | |
| @@ -26,6 +26,10 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 26 | case ports.PointerAxis: | 26 | case ports.PointerAxis: | ||
| 27 | return c.pointerAxis(ctx, v) | 27 | return c.pointerAxis(ctx, v) | ||
| 28 | case ports.SwipeBegin: | 28 | case ports.SwipeBegin: | ||
| 29 | + | if c.dataDrag != nil && !forwardsSwipe(v.Fingers) { | |||
| 30 | + | // The drag owns the pointer: no navigation under it. | |||
| 31 | + | return nil | |||
| 32 | + | } | |||
| 29 | if forwardsSwipe(v.Fingers) { | 33 | if forwardsSwipe(v.Fingers) { | ||
| 30 | return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time) | 34 | return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time) | ||
| 31 | } | 35 | } | ||
| @@ -98,6 +102,11 @@ func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error { | |||||
| 98 | // The dragged window and the others get no pointer events. | 102 | // The dragged window and the others get no pointer events. | ||
| 99 | return c.dragMotion(ctx, v.Time) | 103 | return c.dragMotion(ctx, v.Time) | ||
| 100 | } | 104 | } | ||
| 105 | + | if c.dataDrag != nil { | |||
| 106 | + | // A data drag goes to the surface under the pointer, by its own | |||
| 107 | + | // commands. | |||
| 108 | + | return c.dataDragMotion(ctx, v.Time) | |||
| 109 | + | } | |||
| 101 | id, x, y := c.hit(c.cursorX, c.cursorY) | 110 | id, x, y := c.hit(c.cursorX, c.cursorY) | ||
| 102 | // Layout changes are intentionally re-hit-tested only on motion. | 111 | // Layout changes are intentionally re-hit-tested only on motion. | ||
| 103 | if id != c.pointer { | 112 | if id != c.pointer { | ||
| @@ -123,6 +132,9 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error { | |||||
| 123 | delete(c.buttons, v.Button) | 132 | delete(c.buttons, v.Button) | ||
| 124 | return nil | 133 | return nil | ||
| 125 | } | 134 | } | ||
| 135 | + | if c.dataDrag != nil { | |||
| 136 | + | return c.dataDragButton(ctx, v) | |||
| 137 | + | } | |||
| 126 | if d := c.drag; d != nil { | 138 | if d := c.drag; d != nil { | ||
| 127 | if !v.Pressed && v.Button == d.button { | 139 | if !v.Pressed && v.Button == d.button { | ||
| 128 | delete(c.buttons, v.Button) | 140 | delete(c.buttons, v.Button) | ||
| @@ -160,8 +172,9 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error { | |||||
| 160 | return err | 172 | return err | ||
| 161 | } | 173 | } | ||
| 162 | } | 174 | } | ||
| 175 | + | grab := v.Pressed && c.held() == 0 | |||
| 163 | if v.Pressed { | 176 | if v.Pressed { | ||
| 164 | - | if c.held() == 0 { | 177 | + | if grab { |
| 165 | c.grab = id | 178 | c.grab = id | ||
| 166 | } | 179 | } | ||
| 167 | c.buttons[v.Button] = true | 180 | c.buttons[v.Button] = true | ||
| @@ -170,7 +183,7 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error { | |||||
| 170 | delete(c.buttons, v.Button) | 183 | delete(c.buttons, v.Button) | ||
| 171 | } | 184 | } | ||
| 172 | if id != 0 { | 185 | if id != 0 { | ||
| 173 | - | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil { | 186 | + | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Grab: grab, Time: v.Time}); err != nil { |
| 174 | return err | 187 | return err | ||
| 175 | } | 188 | } | ||
| 176 | // A click on an on-demand layer gives it the keyboard. | 189 | // A click on an on-demand layer gives it the keyboard. | ||
| @@ -226,7 +239,7 @@ func (c *Core) pointerAxis(ctx context.Context, v ports.PointerAxis) error { | |||||
| 226 | } | 239 | } | ||
| 227 | // Scroll goes to the window under the pointer, which has the | 240 | // Scroll goes to the window under the pointer, which has the | ||
| 228 | // pointer focus even mid-drag. None during a window drag. | 241 | // pointer focus even mid-drag. None during a window drag. | ||
| 229 | - | if c.pointer != 0 && c.drag == nil { | 242 | + | if c.pointer != 0 && c.drag == nil && c.dataDrag == nil { |
| 230 | if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil { | 243 | if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil { | ||
| 231 | return err | 244 | return err | ||
| 232 | } | 245 | } | ||
| @@ -244,6 +257,12 @@ func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error { | |||||
| 244 | return err | 257 | return err | ||
| 245 | } | 258 | } | ||
| 246 | } | 259 | } | ||
| 260 | + | if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" { | |||
| 261 | + | // Escape cancels the data drag first, even over the switcher; | |||
| 262 | + | // its release goes nowhere either. | |||
| 263 | + | c.pressed[heldKey(key)] = true | |||
| 264 | + | return c.cancelDataDrag(ctx) | |||
| 265 | + | } | |||
| 247 | if c.switching() && key.Pressed && key.Keysym == "Escape" { | 266 | if c.switching() && key.Pressed && key.Keysym == "Escape" { | ||
| 248 | // Escape cancels the switcher; its release goes nowhere either. | 267 | // Escape cancels the switcher; its release goes nowhere either. | ||
| 249 | c.pressed[heldKey(key)] = true | 268 | c.pressed[heldKey(key)] = true | ||
| @@ -372,7 +391,14 @@ func (c *Core) runBind(ctx context.Context, action Action) error { | |||||
| 372 | return err | 391 | return err | ||
| 373 | } | 392 | } | ||
| 374 | } | 393 | } | ||
| 375 | - | return c.publish(ctx) | 394 | + | if err := c.publish(ctx); err != nil { |
| 395 | + | return err | |||
| 396 | + | } | |||
| 397 | + | if c.dataDrag != nil { | |||
| 398 | + | // A data drag follows the layout under the still cursor. | |||
| 399 | + | return c.rehit(ctx) | |||
| 400 | + | } | |||
| 401 | + | return nil | |||
| 376 | } | 402 | } | ||
| 377 | 403 | ||||
| 378 | // runSpawn starts the programs of a spawn effect of an action applied to w: | 404 | // runSpawn starts the programs of a spawn effect of an action applied to w: | ||
M internal/core/scene.go +5 -1
| @@ -21,7 +21,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 21 | 21 | // layout is what is drawn; settled (same indexes) is where the | |
| 22 | 22 | // windows are going, and alone sizes the configures. | |
| 23 | 23 | layout, settled := sc.shown, sc.settledLayout | |
| 24 | - | scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)} | |
| 24 | + | scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)+1), Layers: shownLayers(sc)} | |
| 25 | 25 | if !sc.mon.previewing() { | |
| 26 | 26 | // Tiles stop at the panels; the overview lays out previews over | |
| 27 | 27 | // the whole output. | |
| @@ -126,6 +126,10 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 126 | 126 | } | |
| 127 | 127 | } | |
| 128 | 128 | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | |
| 129 | + | if icon, ok := c.dataDragIconOn(i, sc); ok { | |
| 130 | + | // The drag icon is over everything, popups and layers included. | |
| 131 | + | scene.Windows = append(scene.Windows, icon) | |
| 132 | + | } | |
| 129 | 133 | scene.CaptureIndicators = c.captureIndicators(sc) | |
| 130 | 134 | scene.Capture = c.captureSceneFor(sc, capture) | |
| 131 | 135 | // The springs of this frame are advanced: a scene shown while one still | |
| @@ -21,7 +21,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 21 | // layout is what is drawn; settled (same indexes) is where the | 21 | // layout is what is drawn; settled (same indexes) is where the | ||
| 22 | // windows are going, and alone sizes the configures. | 22 | // windows are going, and alone sizes the configures. | ||
| 23 | layout, settled := sc.shown, sc.settledLayout | 23 | layout, settled := sc.shown, sc.settledLayout | ||
| 24 | - | scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)} | 24 | + | scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)+1), Layers: shownLayers(sc)} |
| 25 | if !sc.mon.previewing() { | 25 | if !sc.mon.previewing() { | ||
| 26 | // Tiles stop at the panels; the overview lays out previews over | 26 | // Tiles stop at the panels; the overview lays out previews over | ||
| 27 | // the whole output. | 27 | // the whole output. | ||
| @@ -126,6 +126,10 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 126 | } | 126 | } | ||
| 127 | } | 127 | } | ||
| 128 | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | 128 | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | ||
| 129 | + | if icon, ok := c.dataDragIconOn(i, sc); ok { | |||
| 130 | + | // The drag icon is over everything, popups and layers included. | |||
| 131 | + | scene.Windows = append(scene.Windows, icon) | |||
| 132 | + | } | |||
| 129 | scene.CaptureIndicators = c.captureIndicators(sc) | 133 | scene.CaptureIndicators = c.captureIndicators(sc) | ||
| 130 | scene.Capture = c.captureSceneFor(sc, capture) | 134 | scene.Capture = c.captureSceneFor(sc, capture) | ||
| 131 | // The springs of this frame are advanced: a scene shown while one still | 135 | // The springs of this frame are advanced: a scene shown while one still | ||
M internal/core/security.go +2 -0
| @@ -33,6 +33,8 @@ func (c *Core) syncSecurity() bool { | |||
| 33 | 33 | c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{} | |
| 34 | 34 | c.pointer, c.grab, c.pointerOutput = 0, 0, "" | |
| 35 | 35 | c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 0 | |
| 36 | + | // Wayland cancels its own drag when the epoch changes. | |
| 37 | + | c.dataDrag = nil | |
| 36 | 38 | c.pointerAt, c.motionTime = [2]float64{}, 0 | |
| 37 | 39 | c.keyboard = keyboard{} | |
| 38 | 40 | c.constrained, c.constraint = ports.PointerConstrained{}, ports.PointerConstraint{} | |
| @@ -33,6 +33,8 @@ func (c *Core) syncSecurity() bool { | |||||
| 33 | c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{} | 33 | c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{} | ||
| 34 | c.pointer, c.grab, c.pointerOutput = 0, 0, "" | 34 | c.pointer, c.grab, c.pointerOutput = 0, 0, "" | ||
| 35 | c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 0 | 35 | c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 0 | ||
| 36 | + | // Wayland cancels its own drag when the epoch changes. | |||
| 37 | + | c.dataDrag = nil | |||
| 36 | c.pointerAt, c.motionTime = [2]float64{}, 0 | 38 | c.pointerAt, c.motionTime = [2]float64{}, 0 | ||
| 37 | c.keyboard = keyboard{} | 39 | c.keyboard = keyboard{} | ||
| 38 | c.constrained, c.constraint = ports.PointerConstrained{}, ports.PointerConstraint{} | 40 | c.constrained, c.constraint = ports.PointerConstrained{}, ports.PointerConstraint{} | ||
M internal/ports/client_commands.go +3 -0
| @@ -178,6 +178,9 @@ type PointerButtonTo struct { | |||
| 178 | 178 | ID WindowID | |
| 179 | 179 | Button uint32 | |
| 180 | 180 | Pressed bool | |
| 181 | + | // Grab marks the press that starts the implicit grab: no other button | |
| 182 | + | // was held. start_drag must name its serial. | |
| 183 | + | Grab bool | |
| 181 | 184 | // Time is the device timestamp (CLOCK_MONOTONIC). | |
| 182 | 185 | Time time.Duration | |
| 183 | 186 | } | |
| @@ -178,6 +178,9 @@ type PointerButtonTo struct { | |||||
| 178 | ID WindowID | 178 | ID WindowID | ||
| 179 | Button uint32 | 179 | Button uint32 | ||
| 180 | Pressed bool | 180 | Pressed bool | ||
| 181 | + | // Grab marks the press that starts the implicit grab: no other button | |||
| 182 | + | // was held. start_drag must name its serial. | |||
| 183 | + | Grab bool | |||
| 181 | // Time is the device timestamp (CLOCK_MONOTONIC). | 184 | // Time is the device timestamp (CLOCK_MONOTONIC). | ||
| 182 | Time time.Duration | 185 | Time time.Duration | ||
| 183 | } | 186 | } | ||
A internal/ports/dnd.go +69 -0
| @@ -0,0 +1,69 @@ | |||
| 1 | + | package ports | |
| 2 | + | ||
| 3 | + | import "time" | |
| 4 | + | ||
| 5 | + | // Data drag and drop (wl_data_device.start_drag). Wayland validates the | |
| 6 | + | // client's request and tells core; core owns the pointer during the drag: | |
| 7 | + | // it hit-tests, routes the drag to the surface under the pointer, drops on | |
| 8 | + | // the release and cancels on Escape. Wayland speaks the protocol to the | |
| 9 | + | // source and the targets. The icon is a client surface core draws over | |
| 10 | + | // everything at the cursor. | |
| 11 | + | ||
| 12 | + | // DataDragStarted carries wayland → core a client drag answering the | |
| 13 | + | // client's current press. Button is the button of that press, whose | |
| 14 | + | // release drops; Icon is the drag icon's ID (0: none). Core answers with | |
| 15 | + | // DataDragCancel when it refuses the drag. | |
| 16 | + | type DataDragStarted struct { | |
| 17 | + | Button uint32 | |
| 18 | + | Icon WindowID | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | func (DataDragStarted) clientEvent() {} | |
| 22 | + | ||
| 23 | + | // DataDragIcon carries wayland → core the drag icon's geometry after a | |
| 24 | + | // commit: its top left corner relative to the cursor hotspot, and its | |
| 25 | + | // size, in logical pixels. W or H 0 hides it. | |
| 26 | + | type DataDragIcon struct { | |
| 27 | + | ID WindowID | |
| 28 | + | X, Y, W, H int | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | func (DataDragIcon) clientEvent() {} | |
| 32 | + | ||
| 33 | + | // DataDragEnded carries wayland → core the end of a drag core did not | |
| 34 | + | // end itself: the source died or the drag was cancelled. | |
| 35 | + | type DataDragEnded struct{} | |
| 36 | + | ||
| 37 | + | func (DataDragEnded) clientEvent() {} | |
| 38 | + | ||
| 39 | + | // DataDragFocus carries core → wayland the surface under the pointer | |
| 40 | + | // during a drag (0: none), at surface-local X, Y: the old target gets | |
| 41 | + | // leave, the new one enter with an offer. | |
| 42 | + | type DataDragFocus struct { | |
| 43 | + | ID WindowID | |
| 44 | + | X, Y float64 | |
| 45 | + | Time time.Duration | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | func (DataDragFocus) clientCommand() {} | |
| 49 | + | ||
| 50 | + | // DataDragMotion carries core → wayland the pointer over the drag target, | |
| 51 | + | // in its surface coordinates. | |
| 52 | + | type DataDragMotion struct { | |
| 53 | + | ID WindowID | |
| 54 | + | X, Y float64 | |
| 55 | + | Time time.Duration | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | func (DataDragMotion) clientCommand() {} | |
| 59 | + | ||
| 60 | + | // DataDragDrop carries core → wayland the release of the drag button: the | |
| 61 | + | // target gets drop when it accepted the offer, else the drag is cancelled. | |
| 62 | + | type DataDragDrop struct{} | |
| 63 | + | ||
| 64 | + | func (DataDragDrop) clientCommand() {} | |
| 65 | + | ||
| 66 | + | // DataDragCancel carries core → wayland a cancelled or refused drag. | |
| 67 | + | type DataDragCancel struct{} | |
| 68 | + | ||
| 69 | + | func (DataDragCancel) clientCommand() {} | |
| @@ -0,0 +1,69 @@ | |||||
| 1 | + | package ports | |||
| 2 | + | ||||
| 3 | + | import "time" | |||
| 4 | + | ||||
| 5 | + | // Data drag and drop (wl_data_device.start_drag). Wayland validates the | |||
| 6 | + | // client's request and tells core; core owns the pointer during the drag: | |||
| 7 | + | // it hit-tests, routes the drag to the surface under the pointer, drops on | |||
| 8 | + | // the release and cancels on Escape. Wayland speaks the protocol to the | |||
| 9 | + | // source and the targets. The icon is a client surface core draws over | |||
| 10 | + | // everything at the cursor. | |||
| 11 | + | ||||
| 12 | + | // DataDragStarted carries wayland → core a client drag answering the | |||
| 13 | + | // client's current press. Button is the button of that press, whose | |||
| 14 | + | // release drops; Icon is the drag icon's ID (0: none). Core answers with | |||
| 15 | + | // DataDragCancel when it refuses the drag. | |||
| 16 | + | type DataDragStarted struct { | |||
| 17 | + | Button uint32 | |||
| 18 | + | Icon WindowID | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | func (DataDragStarted) clientEvent() {} | |||
| 22 | + | ||||
| 23 | + | // DataDragIcon carries wayland → core the drag icon's geometry after a | |||
| 24 | + | // commit: its top left corner relative to the cursor hotspot, and its | |||
| 25 | + | // size, in logical pixels. W or H 0 hides it. | |||
| 26 | + | type DataDragIcon struct { | |||
| 27 | + | ID WindowID | |||
| 28 | + | X, Y, W, H int | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | func (DataDragIcon) clientEvent() {} | |||
| 32 | + | ||||
| 33 | + | // DataDragEnded carries wayland → core the end of a drag core did not | |||
| 34 | + | // end itself: the source died or the drag was cancelled. | |||
| 35 | + | type DataDragEnded struct{} | |||
| 36 | + | ||||
| 37 | + | func (DataDragEnded) clientEvent() {} | |||
| 38 | + | ||||
| 39 | + | // DataDragFocus carries core → wayland the surface under the pointer | |||
| 40 | + | // during a drag (0: none), at surface-local X, Y: the old target gets | |||
| 41 | + | // leave, the new one enter with an offer. | |||
| 42 | + | type DataDragFocus struct { | |||
| 43 | + | ID WindowID | |||
| 44 | + | X, Y float64 | |||
| 45 | + | Time time.Duration | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | func (DataDragFocus) clientCommand() {} | |||
| 49 | + | ||||
| 50 | + | // DataDragMotion carries core → wayland the pointer over the drag target, | |||
| 51 | + | // in its surface coordinates. | |||
| 52 | + | type DataDragMotion struct { | |||
| 53 | + | ID WindowID | |||
| 54 | + | X, Y float64 | |||
| 55 | + | Time time.Duration | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | func (DataDragMotion) clientCommand() {} | |||
| 59 | + | ||||
| 60 | + | // DataDragDrop carries core → wayland the release of the drag button: the | |||
| 61 | + | // target gets drop when it accepted the offer, else the drag is cancelled. | |||
| 62 | + | type DataDragDrop struct{} | |||
| 63 | + | ||||
| 64 | + | func (DataDragDrop) clientCommand() {} | |||
| 65 | + | ||||
| 66 | + | // DataDragCancel carries core → wayland a cancelled or refused drag. | |||
| 67 | + | type DataDragCancel struct{} | |||
| 68 | + | ||||
| 69 | + | func (DataDragCancel) clientCommand() {} | |||