feat(core): route data drags to the surface under the pointer
- commit
- 4634bd818e1fd84a0f36bf5fb0c0c4d60f594130
- author
- brice · · 2026-10-17 14:17 UTC
- parent
- c2603da
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +692 -4
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 +4 -2
| @@ -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,7 +762,7 @@ 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 { | |
| 763 | - | if c.grab != 0 || c.drag != nil { | |
| 765 | + | if c.grab != 0 || c.drag != nil || c.dataDrag != nil { | |
| 764 | 766 | return nil | |
| 765 | 767 | } | |
| 766 | 768 | id, x, y := c.hit(c.cursorX, c.cursorY) | |
| @@ -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,7 +762,7 @@ 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 { | ||
| 763 | - | if c.grab != 0 || c.drag != nil { | 765 | + | if c.grab != 0 || c.drag != nil || c.dataDrag != nil { |
| 764 | return nil | 766 | return nil | ||
| 765 | } | 767 | } | ||
| 766 | id, x, y := c.hit(c.cursorX, c.cursorY) | 768 | id, x, y := c.hit(c.cursorX, c.cursorY) | ||
A internal/core/datadrag.go +181 -0
| @@ -0,0 +1,181 @@ | |||
| 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; the keyboard keeps its | |
| 21 | + | // focus, clients read the modifiers that choose the action from it; | |
| 22 | + | // - the drag icon, a client surface wayland measures (DataDragIcon), is | |
| 23 | + | // drawn at the cursor over everything. | |
| 24 | + | // | |
| 25 | + | // Layout changes under a still cursor, like an unmapped target, are | |
| 26 | + | // resolved at the next motion. | |
| 27 | + | ||
| 28 | + | // dataDragState is the data drag in progress. | |
| 29 | + | type dataDragState struct { | |
| 30 | + | // button is the pointer button that holds the drag. | |
| 31 | + | button uint32 | |
| 32 | + | // target is the surface the drag was last routed to (0: none). | |
| 33 | + | target WindowID | |
| 34 | + | icon dataDragIcon | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // dataDragIcon is the icon surface: its top left corner relative to the | |
| 38 | + | // cursor and its size, logical px. Hidden while W or H is 0. | |
| 39 | + | type dataDragIcon struct { | |
| 40 | + | id WindowID | |
| 41 | + | x, y, w, h int | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | // startDataDrag answers the start_drag wayland validated: it takes the | |
| 45 | + | // pointer if a client holds a button, else cancels the drag. A drag | |
| 46 | + | // during a window drag or a locked session is refused. | |
| 47 | + | func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error { | |
| 48 | + | button := c.lastButton | |
| 49 | + | if c.dataDrag != nil || c.drag != nil || c.security.Protected || c.grab == 0 || c.held() == 0 || !c.buttons[button] || c.swallow[button] { | |
| 50 | + | return c.command(ctx, ports.DataDragCancel{}) | |
| 51 | + | } | |
| 52 | + | c.dataDrag = &dataDragState{button: button, icon: dataDragIcon{id: v.Icon}} | |
| 53 | + | // Other held buttons release without a client to tell. | |
| 54 | + | for b := range c.buttons { | |
| 55 | + | if b != button { | |
| 56 | + | c.swallow[b] = true | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | c.grab = 0 | |
| 60 | + | // The origin gets its pointer leave, as the drag takes the pointer. | |
| 61 | + | if c.pointer != 0 { | |
| 62 | + | c.pointer = 0 | |
| 63 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { | |
| 64 | + | return err | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | // The surface under the pointer, the origin's usually, is the first | |
| 68 | + | // target. | |
| 69 | + | return c.dataDragRoute(ctx, c.motionTime) | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | // dataDragRoute sends the surface under the cursor as the drag target: a | |
| 73 | + | // focus when it changed, else a motion. | |
| 74 | + | func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error { | |
| 75 | + | d := c.dataDrag | |
| 76 | + | id, x, y := c.hit(c.cursorX, c.cursorY) | |
| 77 | + | switch { | |
| 78 | + | case id != d.target: | |
| 79 | + | d.target = id | |
| 80 | + | return c.command(ctx, ports.DataDragFocus{ID: id, X: x, Y: y, Time: at}) | |
| 81 | + | case id != 0: | |
| 82 | + | return c.command(ctx, ports.DataDragMotion{ID: id, X: x, Y: y, Time: at}) | |
| 83 | + | } | |
| 84 | + | return nil | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | // dataDragMotion follows the cursor: the target gets the motion and the | |
| 88 | + | // icon moves with it. | |
| 89 | + | func (c *Core) dataDragMotion(ctx context.Context, at time.Duration) error { | |
| 90 | + | if err := c.dataDragRoute(ctx, at); err != nil { | |
| 91 | + | return err | |
| 92 | + | } | |
| 93 | + | if c.dataDrag.icon.visible() { | |
| 94 | + | return c.publish(ctx) | |
| 95 | + | } | |
| 96 | + | return nil | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // dataDragButton handles a button event of a data drag: the release of | |
| 100 | + | // the drag button drops; the other buttons do nothing, their releases | |
| 101 | + | // neither. | |
| 102 | + | func (c *Core) dataDragButton(ctx context.Context, v ports.PointerButton) error { | |
| 103 | + | if !v.Pressed { | |
| 104 | + | if v.Button != c.dataDrag.button { | |
| 105 | + | return nil | |
| 106 | + | } | |
| 107 | + | delete(c.buttons, v.Button) | |
| 108 | + | c.dataDrag = nil | |
| 109 | + | if err := c.command(ctx, ports.DataDragDrop{}); err != nil { | |
| 110 | + | return err | |
| 111 | + | } | |
| 112 | + | return c.finishDataDrag(ctx) | |
| 113 | + | } | |
| 114 | + | c.buttons[v.Button] = true | |
| 115 | + | c.swallow[v.Button] = true | |
| 116 | + | return nil | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | // cancelDataDrag ends the drag on Escape. | |
| 120 | + | func (c *Core) cancelDataDrag(ctx context.Context) error { | |
| 121 | + | c.abortDataDrag() | |
| 122 | + | if err := c.command(ctx, ports.DataDragCancel{}); err != nil { | |
| 123 | + | return err | |
| 124 | + | } | |
| 125 | + | return c.finishDataDrag(ctx) | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | // abortDataDrag ends the drag where it is; the held button's release is | |
| 129 | + | // dropped. | |
| 130 | + | func (c *Core) abortDataDrag() { | |
| 131 | + | if d := c.dataDrag; d != nil { | |
| 132 | + | c.swallow[d.button] = true | |
| 133 | + | c.dataDrag = nil | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | // finishDataDrag redraws without the icon and points the pointer at what | |
| 138 | + | // lies under the cursor. | |
| 139 | + | func (c *Core) finishDataDrag(ctx context.Context) error { | |
| 140 | + | if err := c.publish(ctx); err != nil { | |
| 141 | + | return err | |
| 142 | + | } | |
| 143 | + | return c.rehit(ctx) | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | // dataDragGone drops the target that was unmapped: it gets its leave. | |
| 147 | + | func (c *Core) dataDragGone(ctx context.Context, id WindowID) error { | |
| 148 | + | if d := c.dataDrag; d != nil && d.target == id { | |
| 149 | + | d.target = 0 | |
| 150 | + | return c.command(ctx, ports.DataDragFocus{Time: c.motionTime}) | |
| 151 | + | } | |
| 152 | + | return nil | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | // setDataDragIcon records the icon geometry wayland measured. It reports | |
| 156 | + | // whether the scene changes. | |
| 157 | + | func (c *Core) setDataDragIcon(v ports.DataDragIcon) bool { | |
| 158 | + | d := c.dataDrag | |
| 159 | + | if d == nil || d.icon.id == 0 || v.ID != d.icon.id { | |
| 160 | + | return false | |
| 161 | + | } | |
| 162 | + | icon := dataDragIcon{id: v.ID, x: v.X, y: v.Y, w: max(v.W, 0), h: max(v.H, 0)} | |
| 163 | + | if icon == d.icon { | |
| 164 | + | return false | |
| 165 | + | } | |
| 166 | + | d.icon = icon | |
| 167 | + | return true | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | func (i dataDragIcon) visible() bool { return i.id != 0 && i.w > 0 && i.h > 0 } | |
| 171 | + | ||
| 172 | + | // dataDragIconOn is the icon as a window of screen sc (index i) when the | |
| 173 | + | // cursor is on it. | |
| 174 | + | func (c *Core) dataDragIconOn(i int, sc *screen) (ports.SceneWindow, bool) { | |
| 175 | + | d := c.dataDrag | |
| 176 | + | if d == nil || !d.icon.visible() || c.screenAt(c.cursorX, c.cursorY) != i { | |
| 177 | + | return ports.SceneWindow{}, false | |
| 178 | + | } | |
| 179 | + | x, y := int(c.cursorX)-sc.x+d.icon.x, int(c.cursorY)-sc.y+d.icon.y | |
| 180 | + | 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 | |
| 181 | + | } | |
| @@ -0,0 +1,181 @@ | |||||
| 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; the keyboard keeps its | |||
| 21 | + | // focus, clients read the modifiers that choose the action from it; | |||
| 22 | + | // - the drag icon, a client surface wayland measures (DataDragIcon), is | |||
| 23 | + | // drawn at the cursor over everything. | |||
| 24 | + | // | |||
| 25 | + | // Layout changes under a still cursor, like an unmapped target, are | |||
| 26 | + | // resolved at the next motion. | |||
| 27 | + | ||||
| 28 | + | // dataDragState is the data drag in progress. | |||
| 29 | + | type dataDragState struct { | |||
| 30 | + | // button is the pointer button that holds the drag. | |||
| 31 | + | button uint32 | |||
| 32 | + | // target is the surface the drag was last routed to (0: none). | |||
| 33 | + | target WindowID | |||
| 34 | + | icon dataDragIcon | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | // dataDragIcon is the icon surface: its top left corner relative to the | |||
| 38 | + | // cursor and its size, logical px. Hidden while W or H is 0. | |||
| 39 | + | type dataDragIcon struct { | |||
| 40 | + | id WindowID | |||
| 41 | + | x, y, w, h int | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | // startDataDrag answers the start_drag wayland validated: it takes the | |||
| 45 | + | // pointer if a client holds a button, else cancels the drag. A drag | |||
| 46 | + | // during a window drag or a locked session is refused. | |||
| 47 | + | func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error { | |||
| 48 | + | button := c.lastButton | |||
| 49 | + | if c.dataDrag != nil || c.drag != nil || c.security.Protected || c.grab == 0 || c.held() == 0 || !c.buttons[button] || c.swallow[button] { | |||
| 50 | + | return c.command(ctx, ports.DataDragCancel{}) | |||
| 51 | + | } | |||
| 52 | + | c.dataDrag = &dataDragState{button: button, icon: dataDragIcon{id: v.Icon}} | |||
| 53 | + | // Other held buttons release without a client to tell. | |||
| 54 | + | for b := range c.buttons { | |||
| 55 | + | if b != button { | |||
| 56 | + | c.swallow[b] = true | |||
| 57 | + | } | |||
| 58 | + | } | |||
| 59 | + | c.grab = 0 | |||
| 60 | + | // The origin gets its pointer leave, as the drag takes the pointer. | |||
| 61 | + | if c.pointer != 0 { | |||
| 62 | + | c.pointer = 0 | |||
| 63 | + | if err := c.pointerFocus(ctx, ports.PointerFocus{}); err != nil { | |||
| 64 | + | return err | |||
| 65 | + | } | |||
| 66 | + | } | |||
| 67 | + | // The surface under the pointer, the origin's usually, is the first | |||
| 68 | + | // target. | |||
| 69 | + | return c.dataDragRoute(ctx, c.motionTime) | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | // dataDragRoute sends the surface under the cursor as the drag target: a | |||
| 73 | + | // focus when it changed, else a motion. | |||
| 74 | + | func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error { | |||
| 75 | + | d := c.dataDrag | |||
| 76 | + | id, x, y := c.hit(c.cursorX, c.cursorY) | |||
| 77 | + | switch { | |||
| 78 | + | case id != d.target: | |||
| 79 | + | d.target = id | |||
| 80 | + | return c.command(ctx, ports.DataDragFocus{ID: id, X: x, Y: y, Time: at}) | |||
| 81 | + | case id != 0: | |||
| 82 | + | return c.command(ctx, ports.DataDragMotion{ID: id, X: x, Y: y, Time: at}) | |||
| 83 | + | } | |||
| 84 | + | return nil | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | // dataDragMotion follows the cursor: the target gets the motion and the | |||
| 88 | + | // icon moves with it. | |||
| 89 | + | func (c *Core) dataDragMotion(ctx context.Context, at time.Duration) error { | |||
| 90 | + | if err := c.dataDragRoute(ctx, at); err != nil { | |||
| 91 | + | return err | |||
| 92 | + | } | |||
| 93 | + | if c.dataDrag.icon.visible() { | |||
| 94 | + | return c.publish(ctx) | |||
| 95 | + | } | |||
| 96 | + | return nil | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // dataDragButton handles a button event of a data drag: the release of | |||
| 100 | + | // the drag button drops; the other buttons do nothing, their releases | |||
| 101 | + | // neither. | |||
| 102 | + | func (c *Core) dataDragButton(ctx context.Context, v ports.PointerButton) error { | |||
| 103 | + | if !v.Pressed { | |||
| 104 | + | if v.Button != c.dataDrag.button { | |||
| 105 | + | return nil | |||
| 106 | + | } | |||
| 107 | + | delete(c.buttons, v.Button) | |||
| 108 | + | c.dataDrag = nil | |||
| 109 | + | if err := c.command(ctx, ports.DataDragDrop{}); err != nil { | |||
| 110 | + | return err | |||
| 111 | + | } | |||
| 112 | + | return c.finishDataDrag(ctx) | |||
| 113 | + | } | |||
| 114 | + | c.buttons[v.Button] = true | |||
| 115 | + | c.swallow[v.Button] = true | |||
| 116 | + | return nil | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | // cancelDataDrag ends the drag on Escape. | |||
| 120 | + | func (c *Core) cancelDataDrag(ctx context.Context) error { | |||
| 121 | + | c.abortDataDrag() | |||
| 122 | + | if err := c.command(ctx, ports.DataDragCancel{}); err != nil { | |||
| 123 | + | return err | |||
| 124 | + | } | |||
| 125 | + | return c.finishDataDrag(ctx) | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | // abortDataDrag ends the drag where it is; the held button's release is | |||
| 129 | + | // dropped. | |||
| 130 | + | func (c *Core) abortDataDrag() { | |||
| 131 | + | if d := c.dataDrag; d != nil { | |||
| 132 | + | c.swallow[d.button] = true | |||
| 133 | + | c.dataDrag = nil | |||
| 134 | + | } | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | // finishDataDrag redraws without the icon and points the pointer at what | |||
| 138 | + | // lies under the cursor. | |||
| 139 | + | func (c *Core) finishDataDrag(ctx context.Context) error { | |||
| 140 | + | if err := c.publish(ctx); err != nil { | |||
| 141 | + | return err | |||
| 142 | + | } | |||
| 143 | + | return c.rehit(ctx) | |||
| 144 | + | } | |||
| 145 | + | ||||
| 146 | + | // dataDragGone drops the target that was unmapped: it gets its leave. | |||
| 147 | + | func (c *Core) dataDragGone(ctx context.Context, id WindowID) error { | |||
| 148 | + | if d := c.dataDrag; d != nil && d.target == id { | |||
| 149 | + | d.target = 0 | |||
| 150 | + | return c.command(ctx, ports.DataDragFocus{Time: c.motionTime}) | |||
| 151 | + | } | |||
| 152 | + | return nil | |||
| 153 | + | } | |||
| 154 | + | ||||
| 155 | + | // setDataDragIcon records the icon geometry wayland measured. It reports | |||
| 156 | + | // whether the scene changes. | |||
| 157 | + | func (c *Core) setDataDragIcon(v ports.DataDragIcon) bool { | |||
| 158 | + | d := c.dataDrag | |||
| 159 | + | if d == nil || d.icon.id == 0 || v.ID != d.icon.id { | |||
| 160 | + | return false | |||
| 161 | + | } | |||
| 162 | + | icon := dataDragIcon{id: v.ID, x: v.X, y: v.Y, w: max(v.W, 0), h: max(v.H, 0)} | |||
| 163 | + | if icon == d.icon { | |||
| 164 | + | return false | |||
| 165 | + | } | |||
| 166 | + | d.icon = icon | |||
| 167 | + | return true | |||
| 168 | + | } | |||
| 169 | + | ||||
| 170 | + | func (i dataDragIcon) visible() bool { return i.id != 0 && i.w > 0 && i.h > 0 } | |||
| 171 | + | ||||
| 172 | + | // dataDragIconOn is the icon as a window of screen sc (index i) when the | |||
| 173 | + | // cursor is on it. | |||
| 174 | + | func (c *Core) dataDragIconOn(i int, sc *screen) (ports.SceneWindow, bool) { | |||
| 175 | + | d := c.dataDrag | |||
| 176 | + | if d == nil || !d.icon.visible() || c.screenAt(c.cursorX, c.cursorY) != i { | |||
| 177 | + | return ports.SceneWindow{}, false | |||
| 178 | + | } | |||
| 179 | + | x, y := int(c.cursorX)-sc.x+d.icon.x, int(c.cursorY)-sc.y+d.icon.y | |||
| 180 | + | 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 | |||
| 181 | + | } | |||
A internal/core/datadrag_test.go +465 -0
| @@ -0,0 +1,465 @@ | |||
| 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{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}) | |
| 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{} | |
| 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{} | |
| 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{} | |
| 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{} | |
| 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 | + | } | |
| @@ -0,0 +1,465 @@ | |||||
| 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{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}) | |||
| 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{} | |||
| 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{} | |||
| 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{} | |||
| 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{} | |||
| 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 | + | } | |||
M internal/core/input.go +14 -1
| @@ -98,6 +98,11 @@ func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error { | |||
| 98 | 98 | // The dragged window and the others get no pointer events. | |
| 99 | 99 | return c.dragMotion(ctx, v.Time) | |
| 100 | 100 | } | |
| 101 | + | if c.dataDrag != nil { | |
| 102 | + | // A data drag goes to the surface under the pointer, by its own | |
| 103 | + | // commands. | |
| 104 | + | return c.dataDragMotion(ctx, v.Time) | |
| 105 | + | } | |
| 101 | 106 | id, x, y := c.hit(c.cursorX, c.cursorY) | |
| 102 | 107 | // Layout changes are intentionally re-hit-tested only on motion. | |
| 103 | 108 | if id != c.pointer { | |
| @@ -123,6 +128,9 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error { | |||
| 123 | 128 | delete(c.buttons, v.Button) | |
| 124 | 129 | return nil | |
| 125 | 130 | } | |
| 131 | + | if c.dataDrag != nil { | |
| 132 | + | return c.dataDragButton(ctx, v) | |
| 133 | + | } | |
| 126 | 134 | if d := c.drag; d != nil { | |
| 127 | 135 | if !v.Pressed && v.Button == d.button { | |
| 128 | 136 | delete(c.buttons, v.Button) | |
| @@ -226,7 +234,7 @@ func (c *Core) pointerAxis(ctx context.Context, v ports.PointerAxis) error { | |||
| 226 | 234 | } | |
| 227 | 235 | // Scroll goes to the window under the pointer, which has the | |
| 228 | 236 | // pointer focus even mid-drag. None during a window drag. | |
| 229 | - | if c.pointer != 0 && c.drag == nil { | |
| 237 | + | if c.pointer != 0 && c.drag == nil && c.dataDrag == nil { | |
| 230 | 238 | if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil { | |
| 231 | 239 | return err | |
| 232 | 240 | } | |
| @@ -249,6 +257,11 @@ func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error { | |||
| 249 | 257 | c.pressed[heldKey(key)] = true | |
| 250 | 258 | return c.abortSwitcher(ctx) | |
| 251 | 259 | } | |
| 260 | + | if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" { | |
| 261 | + | // Escape cancels the data drag; its release goes nowhere either. | |
| 262 | + | c.pressed[heldKey(key)] = true | |
| 263 | + | return c.cancelDataDrag(ctx) | |
| 264 | + | } | |
| 252 | 265 | if c.drag != nil && key.Pressed && key.Keysym == "Escape" { | |
| 253 | 266 | // Escape cancels the drag; its release goes nowhere either. | |
| 254 | 267 | c.cancelDrag() | |
| @@ -98,6 +98,11 @@ func (c *Core) pointerMotion(ctx context.Context, v ports.PointerMotion) error { | |||||
| 98 | // The dragged window and the others get no pointer events. | 98 | // The dragged window and the others get no pointer events. | ||
| 99 | return c.dragMotion(ctx, v.Time) | 99 | return c.dragMotion(ctx, v.Time) | ||
| 100 | } | 100 | } | ||
| 101 | + | if c.dataDrag != nil { | |||
| 102 | + | // A data drag goes to the surface under the pointer, by its own | |||
| 103 | + | // commands. | |||
| 104 | + | return c.dataDragMotion(ctx, v.Time) | |||
| 105 | + | } | |||
| 101 | id, x, y := c.hit(c.cursorX, c.cursorY) | 106 | id, x, y := c.hit(c.cursorX, c.cursorY) | ||
| 102 | // Layout changes are intentionally re-hit-tested only on motion. | 107 | // Layout changes are intentionally re-hit-tested only on motion. | ||
| 103 | if id != c.pointer { | 108 | if id != c.pointer { | ||
| @@ -123,6 +128,9 @@ func (c *Core) pointerButton(ctx context.Context, v ports.PointerButton) error { | |||||
| 123 | delete(c.buttons, v.Button) | 128 | delete(c.buttons, v.Button) | ||
| 124 | return nil | 129 | return nil | ||
| 125 | } | 130 | } | ||
| 131 | + | if c.dataDrag != nil { | |||
| 132 | + | return c.dataDragButton(ctx, v) | |||
| 133 | + | } | |||
| 126 | if d := c.drag; d != nil { | 134 | if d := c.drag; d != nil { | ||
| 127 | if !v.Pressed && v.Button == d.button { | 135 | if !v.Pressed && v.Button == d.button { | ||
| 128 | delete(c.buttons, v.Button) | 136 | delete(c.buttons, v.Button) | ||
| @@ -226,7 +234,7 @@ func (c *Core) pointerAxis(ctx context.Context, v ports.PointerAxis) error { | |||||
| 226 | } | 234 | } | ||
| 227 | // Scroll goes to the window under the pointer, which has the | 235 | // Scroll goes to the window under the pointer, which has the | ||
| 228 | // pointer focus even mid-drag. None during a window drag. | 236 | // pointer focus even mid-drag. None during a window drag. | ||
| 229 | - | if c.pointer != 0 && c.drag == nil { | 237 | + | 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 { | 238 | if err := c.command(ctx, ports.PointerAxisTo{ID: c.pointer, Axis: v}); err != nil { | ||
| 231 | return err | 239 | return err | ||
| 232 | } | 240 | } | ||
| @@ -249,6 +257,11 @@ func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error { | |||||
| 249 | c.pressed[heldKey(key)] = true | 257 | c.pressed[heldKey(key)] = true | ||
| 250 | return c.abortSwitcher(ctx) | 258 | return c.abortSwitcher(ctx) | ||
| 251 | } | 259 | } | ||
| 260 | + | if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" { | |||
| 261 | + | // Escape cancels the data drag; its release goes nowhere either. | |||
| 262 | + | c.pressed[heldKey(key)] = true | |||
| 263 | + | return c.cancelDataDrag(ctx) | |||
| 264 | + | } | |||
| 252 | if c.drag != nil && key.Pressed && key.Keysym == "Escape" { | 265 | if c.drag != nil && key.Pressed && key.Keysym == "Escape" { | ||
| 253 | // Escape cancels the drag; its release goes nowhere either. | 266 | // Escape cancels the drag; its release goes nowhere either. | ||
| 254 | c.cancelDrag() | 267 | c.cancelDrag() | ||
M internal/core/scene.go +4 -0
| @@ -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 | |
| @@ -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{} | ||