Merge pull request #20: fix(wayland): honour wl_surface.set_input_region
# Conflicts: # internal/adapters/wayland/fifo.go # internal/adapters/wayland/surface.go
13 files · +761 -72
M internal/adapters/wayland/constraints.go +60 -16
| @@ -138,7 +138,7 @@ func (s *Server) regionBox(reg *wayland.Region) ports.Rect { | |||
| 138 | 138 | return ports.Rect{} | |
| 139 | 139 | } | |
| 140 | 140 | if g := s.regions[reg.Resource]; g != nil { | |
| 141 | - | return g.box | |
| 141 | + | return g.boxRect() | |
| 142 | 142 | } | |
| 143 | 143 | return ports.Rect{} | |
| 144 | 144 | } | |
| @@ -205,6 +205,26 @@ func (s *Server) dropConstraint(c *constraint) { | |||
| 205 | 205 | s.updateConstraint() | |
| 206 | 206 | } | |
| 207 | 207 | ||
| 208 | + | // deactivateConstraint ends the active constraint; a oneshot cannot reactivate. | |
| 209 | + | func (s *Server) deactivateConstraint() { | |
| 210 | + | old := s.constraint | |
| 211 | + | if old == nil { | |
| 212 | + | return | |
| 213 | + | } | |
| 214 | + | old.active = false | |
| 215 | + | if !old.persistent { | |
| 216 | + | old.defunct = true | |
| 217 | + | } | |
| 218 | + | if old.lock != nil && old.lock.Resource.Alive() { | |
| 219 | + | old.lock.SendUnlocked() | |
| 220 | + | } | |
| 221 | + | if old.confine != nil && old.confine.Resource.Alive() { | |
| 222 | + | old.confine.SendUnconfined() | |
| 223 | + | } | |
| 224 | + | s.constraint = nil | |
| 225 | + | s.emit(ports.PointerConstrained{}) | |
| 226 | + | } | |
| 227 | + | ||
| 208 | 228 | // updateConstraint activates the constraint of the window holding both | |
| 209 | 229 | // pointer and keyboard focus, and deactivates any other. | |
| 210 | 230 | func (s *Server) updateConstraint() { | |
| @@ -217,23 +237,11 @@ func (s *Server) updateConstraint() { | |||
| 217 | 237 | if want == s.constraint { | |
| 218 | 238 | return | |
| 219 | 239 | } | |
| 220 | - | if old := s.constraint; old != nil { | |
| 221 | - | old.active = false | |
| 222 | - | if !old.persistent { | |
| 223 | - | old.defunct = true | |
| 224 | - | } | |
| 225 | - | if old.lock != nil && old.lock.Resource.Alive() { | |
| 226 | - | old.lock.SendUnlocked() | |
| 227 | - | } | |
| 228 | - | if old.confine != nil && old.confine.Resource.Alive() { | |
| 229 | - | old.confine.SendUnconfined() | |
| 230 | - | } | |
| 231 | - | } | |
| 232 | - | s.constraint = want | |
| 240 | + | s.deactivateConstraint() | |
| 233 | 241 | if want == nil { | |
| 234 | - | s.emit(ports.PointerConstrained{}) | |
| 235 | 242 | return | |
| 236 | 243 | } | |
| 244 | + | s.constraint = want | |
| 237 | 245 | want.active = true | |
| 238 | 246 | if want.lock != nil { | |
| 239 | 247 | want.lock.SendLocked() | |
| @@ -258,6 +266,28 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 258 | 266 | r.X -= x.geometry.X | |
| 259 | 267 | r.Y -= x.geometry.Y | |
| 260 | 268 | } | |
| 269 | + | // Core supports one confinement rect, so this bounds the intersection; | |
| 270 | + | // contains uses the exact input region to decide activation. | |
| 271 | + | if mode == ports.ConstraintConfine { | |
| 272 | + | if all, rects := c.surface.effectiveInput(); !all { | |
| 273 | + | var box ports.Rect | |
| 274 | + | for _, input := range rects { | |
| 275 | + | if r.W > 0 && r.H > 0 { | |
| 276 | + | input = intersectRect(input, r) | |
| 277 | + | } | |
| 278 | + | if input.W <= 0 { | |
| 279 | + | continue | |
| 280 | + | } | |
| 281 | + | if box.W == 0 { | |
| 282 | + | box = input | |
| 283 | + | } else { | |
| 284 | + | x0, y0 := min(box.X, input.X), min(box.Y, input.Y) | |
| 285 | + | box = ports.Rect{X: x0, Y: y0, W: max(box.X+box.W, input.X+input.W) - x0, H: max(box.Y+box.H, input.Y+input.H) - y0} | |
| 286 | + | } | |
| 287 | + | } | |
| 288 | + | r = box | |
| 289 | + | } | |
| 290 | + | } | |
| 261 | 291 | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | |
| 262 | 292 | } | |
| 263 | 293 | ||
| @@ -265,10 +295,24 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 265 | 295 | // activates only there, so it never warps the pointer. | |
| 266 | 296 | func (c *constraint) contains(w *window) bool { | |
| 267 | 297 | r := c.region | |
| 298 | + | x, y := w.surfacePoint(w.xdg.server.pointerX, w.xdg.server.pointerY) | |
| 299 | + | all, rects := c.surface.effectiveInput() | |
| 300 | + | if !all { | |
| 301 | + | localX, localY := x-float64(w.xdg.geometry.X), y-float64(w.xdg.geometry.Y) | |
| 302 | + | inside := false | |
| 303 | + | for _, input := range rects { | |
| 304 | + | if localX >= float64(input.X) && localX < float64(input.X+input.W) && localY >= float64(input.Y) && localY < float64(input.Y+input.H) { | |
| 305 | + | inside = true | |
| 306 | + | break | |
| 307 | + | } | |
| 308 | + | } | |
| 309 | + | if !inside { | |
| 310 | + | return false | |
| 311 | + | } | |
| 312 | + | } | |
| 268 | 313 | if r.W <= 0 || r.H <= 0 { | |
| 269 | 314 | return true | |
| 270 | 315 | } | |
| 271 | - | x, y := w.surfacePoint(w.xdg.server.pointerX, w.xdg.server.pointerY) | |
| 272 | 316 | return x >= float64(r.X) && x < float64(r.X+r.W) && y >= float64(r.Y) && y < float64(r.Y+r.H) | |
| 273 | 317 | } | |
| 274 | 318 | ||
| @@ -138,7 +138,7 @@ func (s *Server) regionBox(reg *wayland.Region) ports.Rect { | |||||
| 138 | return ports.Rect{} | 138 | return ports.Rect{} | ||
| 139 | } | 139 | } | ||
| 140 | if g := s.regions[reg.Resource]; g != nil { | 140 | if g := s.regions[reg.Resource]; g != nil { | ||
| 141 | - | return g.box | 141 | + | return g.boxRect() |
| 142 | } | 142 | } | ||
| 143 | return ports.Rect{} | 143 | return ports.Rect{} | ||
| 144 | } | 144 | } | ||
| @@ -205,6 +205,26 @@ func (s *Server) dropConstraint(c *constraint) { | |||||
| 205 | s.updateConstraint() | 205 | s.updateConstraint() | ||
| 206 | } | 206 | } | ||
| 207 | 207 | ||||
| 208 | + | // deactivateConstraint ends the active constraint; a oneshot cannot reactivate. | |||
| 209 | + | func (s *Server) deactivateConstraint() { | |||
| 210 | + | old := s.constraint | |||
| 211 | + | if old == nil { | |||
| 212 | + | return | |||
| 213 | + | } | |||
| 214 | + | old.active = false | |||
| 215 | + | if !old.persistent { | |||
| 216 | + | old.defunct = true | |||
| 217 | + | } | |||
| 218 | + | if old.lock != nil && old.lock.Resource.Alive() { | |||
| 219 | + | old.lock.SendUnlocked() | |||
| 220 | + | } | |||
| 221 | + | if old.confine != nil && old.confine.Resource.Alive() { | |||
| 222 | + | old.confine.SendUnconfined() | |||
| 223 | + | } | |||
| 224 | + | s.constraint = nil | |||
| 225 | + | s.emit(ports.PointerConstrained{}) | |||
| 226 | + | } | |||
| 227 | + | ||||
| 208 | // updateConstraint activates the constraint of the window holding both | 228 | // updateConstraint activates the constraint of the window holding both | ||
| 209 | // pointer and keyboard focus, and deactivates any other. | 229 | // pointer and keyboard focus, and deactivates any other. | ||
| 210 | func (s *Server) updateConstraint() { | 230 | func (s *Server) updateConstraint() { | ||
| @@ -217,23 +237,11 @@ func (s *Server) updateConstraint() { | |||||
| 217 | if want == s.constraint { | 237 | if want == s.constraint { | ||
| 218 | return | 238 | return | ||
| 219 | } | 239 | } | ||
| 220 | - | if old := s.constraint; old != nil { | 240 | + | s.deactivateConstraint() |
| 221 | - | old.active = false | |||
| 222 | - | if !old.persistent { | |||
| 223 | - | old.defunct = true | |||
| 224 | - | } | |||
| 225 | - | if old.lock != nil && old.lock.Resource.Alive() { | |||
| 226 | - | old.lock.SendUnlocked() | |||
| 227 | - | } | |||
| 228 | - | if old.confine != nil && old.confine.Resource.Alive() { | |||
| 229 | - | old.confine.SendUnconfined() | |||
| 230 | - | } | |||
| 231 | - | } | |||
| 232 | - | s.constraint = want | |||
| 233 | if want == nil { | 241 | if want == nil { | ||
| 234 | - | s.emit(ports.PointerConstrained{}) | |||
| 235 | return | 242 | return | ||
| 236 | } | 243 | } | ||
| 244 | + | s.constraint = want | |||
| 237 | want.active = true | 245 | want.active = true | ||
| 238 | if want.lock != nil { | 246 | if want.lock != nil { | ||
| 239 | want.lock.SendLocked() | 247 | want.lock.SendLocked() | ||
| @@ -258,6 +266,28 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 258 | r.X -= x.geometry.X | 266 | r.X -= x.geometry.X | ||
| 259 | r.Y -= x.geometry.Y | 267 | r.Y -= x.geometry.Y | ||
| 260 | } | 268 | } | ||
| 269 | + | // Core supports one confinement rect, so this bounds the intersection; | |||
| 270 | + | // contains uses the exact input region to decide activation. | |||
| 271 | + | if mode == ports.ConstraintConfine { | |||
| 272 | + | if all, rects := c.surface.effectiveInput(); !all { | |||
| 273 | + | var box ports.Rect | |||
| 274 | + | for _, input := range rects { | |||
| 275 | + | if r.W > 0 && r.H > 0 { | |||
| 276 | + | input = intersectRect(input, r) | |||
| 277 | + | } | |||
| 278 | + | if input.W <= 0 { | |||
| 279 | + | continue | |||
| 280 | + | } | |||
| 281 | + | if box.W == 0 { | |||
| 282 | + | box = input | |||
| 283 | + | } else { | |||
| 284 | + | x0, y0 := min(box.X, input.X), min(box.Y, input.Y) | |||
| 285 | + | box = ports.Rect{X: x0, Y: y0, W: max(box.X+box.W, input.X+input.W) - x0, H: max(box.Y+box.H, input.Y+input.H) - y0} | |||
| 286 | + | } | |||
| 287 | + | } | |||
| 288 | + | r = box | |||
| 289 | + | } | |||
| 290 | + | } | |||
| 261 | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | 291 | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | ||
| 262 | } | 292 | } | ||
| 263 | 293 | ||||
| @@ -265,10 +295,24 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 265 | // activates only there, so it never warps the pointer. | 295 | // activates only there, so it never warps the pointer. | ||
| 266 | func (c *constraint) contains(w *window) bool { | 296 | func (c *constraint) contains(w *window) bool { | ||
| 267 | r := c.region | 297 | r := c.region | ||
| 298 | + | x, y := w.surfacePoint(w.xdg.server.pointerX, w.xdg.server.pointerY) | |||
| 299 | + | all, rects := c.surface.effectiveInput() | |||
| 300 | + | if !all { | |||
| 301 | + | localX, localY := x-float64(w.xdg.geometry.X), y-float64(w.xdg.geometry.Y) | |||
| 302 | + | inside := false | |||
| 303 | + | for _, input := range rects { | |||
| 304 | + | if localX >= float64(input.X) && localX < float64(input.X+input.W) && localY >= float64(input.Y) && localY < float64(input.Y+input.H) { | |||
| 305 | + | inside = true | |||
| 306 | + | break | |||
| 307 | + | } | |||
| 308 | + | } | |||
| 309 | + | if !inside { | |||
| 310 | + | return false | |||
| 311 | + | } | |||
| 312 | + | } | |||
| 268 | if r.W <= 0 || r.H <= 0 { | 313 | if r.W <= 0 || r.H <= 0 { | ||
| 269 | return true | 314 | return true | ||
| 270 | } | 315 | } | ||
| 271 | - | x, y := w.surfacePoint(w.xdg.server.pointerX, w.xdg.server.pointerY) | |||
| 272 | return x >= float64(r.X) && x < float64(r.X+r.W) && y >= float64(r.Y) && y < float64(r.Y+r.H) | 316 | return x >= float64(r.X) && x < float64(r.X+r.W) && y >= float64(r.Y) && y < float64(r.Y+r.H) | ||
| 273 | } | 317 | } | ||
| 274 | 318 | ||||
M internal/adapters/wayland/fifo.go +39 -33
| @@ -198,39 +198,41 @@ func (s *Server) surfaceOf(w *wayland.Surface) *surface { | |||
| 198 | 198 | ||
| 199 | 199 | // update is a surface's pending state taken out by a commit that waits. | |
| 200 | 200 | type update struct { | |
| 201 | - | attached bool | |
| 202 | - | buffer *wayland.Buffer | |
| 203 | - | scale int | |
| 204 | - | async bool | |
| 205 | - | kind uint32 | |
| 206 | - | callbacks []*wayland.Callback | |
| 207 | - | vp *viewport | |
| 208 | - | vpW, vpH int32 | |
| 209 | - | vpSet bool | |
| 210 | - | vpSrc [4]server.Fixed | |
| 211 | - | vpCrop bool | |
| 212 | - | layout []childLayout | |
| 213 | - | deps []*update | |
| 214 | - | owner *surface | |
| 215 | - | prev *update | |
| 216 | - | synced, bound bool | |
| 217 | - | xdg *xdgSurface | |
| 218 | - | geometry ports.Rect | |
| 219 | - | cons *constraint | |
| 220 | - | region *ports.Rect | |
| 221 | - | layer *layerSurface | |
| 222 | - | layerNext layerState | |
| 223 | - | barrier bool | |
| 224 | - | wait bool | |
| 225 | - | at time.Time | |
| 226 | - | feedback []*presentationtime.WpPresentationFeedback | |
| 227 | - | sync *commitSync | |
| 228 | - | color SurfaceColor | |
| 229 | - | representation surfaceRepresentation | |
| 230 | - | damage []ports.Rect | |
| 231 | - | bufDamage []ports.Rect | |
| 232 | - | readyGeneration uint64 | |
| 233 | - | readyResult bool | |
| 201 | + | inputSet, inputAll bool | |
| 202 | + | inputRects []ports.Rect | |
| 203 | + | attached bool | |
| 204 | + | buffer *wayland.Buffer | |
| 205 | + | scale int | |
| 206 | + | async bool | |
| 207 | + | kind uint32 | |
| 208 | + | callbacks []*wayland.Callback | |
| 209 | + | vp *viewport | |
| 210 | + | vpW, vpH int32 | |
| 211 | + | vpSet bool | |
| 212 | + | vpSrc [4]server.Fixed | |
| 213 | + | vpCrop bool | |
| 214 | + | layout []childLayout | |
| 215 | + | deps []*update | |
| 216 | + | owner *surface | |
| 217 | + | prev *update | |
| 218 | + | synced, bound bool | |
| 219 | + | xdg *xdgSurface | |
| 220 | + | geometry ports.Rect | |
| 221 | + | cons *constraint | |
| 222 | + | region *ports.Rect | |
| 223 | + | layer *layerSurface | |
| 224 | + | layerNext layerState | |
| 225 | + | barrier bool | |
| 226 | + | wait bool | |
| 227 | + | at time.Time | |
| 228 | + | feedback []*presentationtime.WpPresentationFeedback | |
| 229 | + | sync *commitSync | |
| 230 | + | color SurfaceColor | |
| 231 | + | representation surfaceRepresentation | |
| 232 | + | damage []ports.Rect | |
| 233 | + | bufDamage []ports.Rect | |
| 234 | + | readyGeneration uint64 | |
| 235 | + | readyResult bool | |
| 234 | 236 | } | |
| 235 | 237 | ||
| 236 | 238 | type childLayout struct { | |
| @@ -266,6 +268,9 @@ func sameLayout(a, b []childLayout) bool { | |||
| 266 | 268 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | |
| 267 | 269 | func (s *surface) takePending() update { | |
| 268 | 270 | u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage, feedback: s.pendingFeedback, sync: s.pendingSync, color: s.pendingColor, representation: s.pendingRepresentation} | |
| 271 | + | u.inputSet, u.inputAll, u.inputRects = s.pendingInputSet, s.pendingInputAll, s.pendingInputRects | |
| 272 | + | s.pendingInputSet = false | |
| 273 | + | s.pendingInputRects = nil | |
| 269 | 274 | s.attached, s.pending, s.callbacks = false, nil, nil | |
| 270 | 275 | s.pendingFeedback, s.pendingSync = nil, nil | |
| 271 | 276 | s.pendingDamage, s.pendingBufDamage = nil, nil | |
| @@ -289,6 +294,7 @@ func (s *surface) takePending() update { | |||
| 289 | 294 | // putPending makes an update the surface's pending state again. Parts | |
| 290 | 295 | // whose object is gone (viewport, child, role, constraint) are dropped. | |
| 291 | 296 | func (s *surface) putPending(u update) { | |
| 297 | + | s.pendingInputSet, s.pendingInputAll, s.pendingInputRects = u.inputSet, u.inputAll, u.inputRects | |
| 292 | 298 | s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks | |
| 293 | 299 | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | |
| 294 | 300 | s.pendingColor = u.color | |
| @@ -198,39 +198,41 @@ func (s *Server) surfaceOf(w *wayland.Surface) *surface { | |||||
| 198 | 198 | ||||
| 199 | // update is a surface's pending state taken out by a commit that waits. | 199 | // update is a surface's pending state taken out by a commit that waits. | ||
| 200 | type update struct { | 200 | type update struct { | ||
| 201 | - | attached bool | 201 | + | inputSet, inputAll bool |
| 202 | - | buffer *wayland.Buffer | 202 | + | inputRects []ports.Rect |
| 203 | - | scale int | 203 | + | attached bool |
| 204 | - | async bool | 204 | + | buffer *wayland.Buffer |
| 205 | - | kind uint32 | 205 | + | scale int |
| 206 | - | callbacks []*wayland.Callback | 206 | + | async bool |
| 207 | - | vp *viewport | 207 | + | kind uint32 |
| 208 | - | vpW, vpH int32 | 208 | + | callbacks []*wayland.Callback |
| 209 | - | vpSet bool | 209 | + | vp *viewport |
| 210 | - | vpSrc [4]server.Fixed | 210 | + | vpW, vpH int32 |
| 211 | - | vpCrop bool | 211 | + | vpSet bool |
| 212 | - | layout []childLayout | 212 | + | vpSrc [4]server.Fixed |
| 213 | - | deps []*update | 213 | + | vpCrop bool |
| 214 | - | owner *surface | 214 | + | layout []childLayout |
| 215 | - | prev *update | 215 | + | deps []*update |
| 216 | - | synced, bound bool | 216 | + | owner *surface |
| 217 | - | xdg *xdgSurface | 217 | + | prev *update |
| 218 | - | geometry ports.Rect | 218 | + | synced, bound bool |
| 219 | - | cons *constraint | 219 | + | xdg *xdgSurface |
| 220 | - | region *ports.Rect | 220 | + | geometry ports.Rect |
| 221 | - | layer *layerSurface | 221 | + | cons *constraint |
| 222 | - | layerNext layerState | 222 | + | region *ports.Rect |
| 223 | - | barrier bool | 223 | + | layer *layerSurface |
| 224 | - | wait bool | 224 | + | layerNext layerState |
| 225 | - | at time.Time | 225 | + | barrier bool |
| 226 | - | feedback []*presentationtime.WpPresentationFeedback | 226 | + | wait bool |
| 227 | - | sync *commitSync | 227 | + | at time.Time |
| 228 | - | color SurfaceColor | 228 | + | feedback []*presentationtime.WpPresentationFeedback |
| 229 | - | representation surfaceRepresentation | 229 | + | sync *commitSync |
| 230 | - | damage []ports.Rect | 230 | + | color SurfaceColor |
| 231 | - | bufDamage []ports.Rect | 231 | + | representation surfaceRepresentation |
| 232 | - | readyGeneration uint64 | 232 | + | damage []ports.Rect |
| 233 | - | readyResult bool | 233 | + | bufDamage []ports.Rect |
| 234 | + | readyGeneration uint64 | |||
| 235 | + | readyResult bool | |||
| 234 | } | 236 | } | ||
| 235 | 237 | ||||
| 236 | type childLayout struct { | 238 | type childLayout struct { | ||
| @@ -266,6 +268,9 @@ func sameLayout(a, b []childLayout) bool { | |||||
| 266 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | 268 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | ||
| 267 | func (s *surface) takePending() update { | 269 | func (s *surface) takePending() update { | ||
| 268 | u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage, feedback: s.pendingFeedback, sync: s.pendingSync, color: s.pendingColor, representation: s.pendingRepresentation} | 270 | u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage, feedback: s.pendingFeedback, sync: s.pendingSync, color: s.pendingColor, representation: s.pendingRepresentation} | ||
| 271 | + | u.inputSet, u.inputAll, u.inputRects = s.pendingInputSet, s.pendingInputAll, s.pendingInputRects | |||
| 272 | + | s.pendingInputSet = false | |||
| 273 | + | s.pendingInputRects = nil | |||
| 269 | s.attached, s.pending, s.callbacks = false, nil, nil | 274 | s.attached, s.pending, s.callbacks = false, nil, nil | ||
| 270 | s.pendingFeedback, s.pendingSync = nil, nil | 275 | s.pendingFeedback, s.pendingSync = nil, nil | ||
| 271 | s.pendingDamage, s.pendingBufDamage = nil, nil | 276 | s.pendingDamage, s.pendingBufDamage = nil, nil | ||
| @@ -289,6 +294,7 @@ func (s *surface) takePending() update { | |||||
| 289 | // putPending makes an update the surface's pending state again. Parts | 294 | // putPending makes an update the surface's pending state again. Parts | ||
| 290 | // whose object is gone (viewport, child, role, constraint) are dropped. | 295 | // whose object is gone (viewport, child, role, constraint) are dropped. | ||
| 291 | func (s *surface) putPending(u update) { | 296 | func (s *surface) putPending(u update) { | ||
| 297 | + | s.pendingInputSet, s.pendingInputAll, s.pendingInputRects = u.inputSet, u.inputAll, u.inputRects | |||
| 292 | s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks | 298 | s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks | ||
| 293 | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | 299 | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | ||
| 294 | s.pendingColor = u.color | 300 | s.pendingColor = u.color | ||
M internal/adapters/wayland/globals.go +28 -10
| @@ -73,7 +73,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||
| 73 | 73 | type compositor struct{ server *Server } | |
| 74 | 74 | ||
| 75 | 75 | func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) { | |
| 76 | - | state := &surface{server: c.server, bufferScale: 1} | |
| 76 | + | state := &surface{server: c.server, bufferScale: 1, inputAll: true} | |
| 77 | 77 | if w, err := wayland.NewSurface(r.Client(), r.Version(), id, state); err == nil { | |
| 78 | 78 | c.server.nextSurface++ | |
| 79 | 79 | state.identity = c.server.nextSurface | |
| @@ -125,6 +125,7 @@ func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, par | |||
| 125 | 125 | up.sub.pendingLayout = append(append([]childLayout(nil), up.sub.pendingLayout...), childLayout{child: state}) | |
| 126 | 126 | // The child follows its root window's output and scale. | |
| 127 | 127 | state.sendScale() | |
| 128 | + | up.emitInput() | |
| 128 | 129 | sub.OnDestroy = func() { | |
| 129 | 130 | if state.sub.role != sub { | |
| 130 | 131 | return | |
| @@ -218,23 +219,40 @@ func (s subsurface) restack(r *wayland.Subsurface, sibling *wayland.Surface, abo | |||
| 218 | 219 | p.sub.pendingLayout = list | |
| 219 | 220 | } | |
| 220 | 221 | ||
| 221 | - | // region keeps the bounding box of the rectangles added: pointer | |
| 222 | - | // confinement is the only user, and subtracted areas are ignored. | |
| 223 | - | type region struct{ box ports.Rect } | |
| 222 | + | // region stores the exact union of added rectangles. | |
| 223 | + | type region struct{ rects []ports.Rect } | |
| 224 | 224 | ||
| 225 | 225 | func (*region) Destroy(*wayland.Region) {} | |
| 226 | 226 | func (g *region) Add(_ *wayland.Region, x, y, w, h int32) { | |
| 227 | + | if w > 0 && h > 0 { | |
| 228 | + | g.rects = append(g.rects, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)}) | |
| 229 | + | } | |
| 230 | + | } | |
| 231 | + | func (g *region) Subtract(_ *wayland.Region, x, y, w, h int32) { | |
| 227 | 232 | if w <= 0 || h <= 0 { | |
| 228 | 233 | return | |
| 229 | 234 | } | |
| 230 | - | r := ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)} | |
| 231 | - | if g.box.W > 0 { | |
| 232 | - | x0, y0 := min(g.box.X, r.X), min(g.box.Y, r.Y) | |
| 233 | - | r = ports.Rect{X: x0, Y: y0, W: max(g.box.X+g.box.W, r.X+r.W) - x0, H: max(g.box.Y+g.box.H, r.Y+r.H) - y0} | |
| 235 | + | cut := ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)} | |
| 236 | + | var out []ports.Rect | |
| 237 | + | for _, r := range g.rects { | |
| 238 | + | i := intersectRect(r, cut) | |
| 239 | + | if i.W == 0 { | |
| 240 | + | out = append(out, r) | |
| 241 | + | continue | |
| 242 | + | } | |
| 243 | + | for _, piece := range []ports.Rect{ | |
| 244 | + | {X: r.X, Y: r.Y, W: r.W, H: i.Y - r.Y}, | |
| 245 | + | {X: r.X, Y: i.Y + i.H, W: r.W, H: r.Y + r.H - i.Y - i.H}, | |
| 246 | + | {X: r.X, Y: i.Y, W: i.X - r.X, H: i.H}, | |
| 247 | + | {X: i.X + i.W, Y: i.Y, W: r.X + r.W - i.X - i.W, H: i.H}, | |
| 248 | + | } { | |
| 249 | + | if piece.W > 0 && piece.H > 0 { | |
| 250 | + | out = append(out, piece) | |
| 251 | + | } | |
| 252 | + | } | |
| 234 | 253 | } | |
| 235 | - | g.box = r | |
| 254 | + | g.rects = out | |
| 236 | 255 | } | |
| 237 | - | func (*region) Subtract(*wayland.Region, int32, int32, int32, int32) {} | |
| 238 | 256 | ||
| 239 | 257 | type shm struct{ server *Server } | |
| 240 | 258 | ||
| @@ -73,7 +73,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||||
| 73 | type compositor struct{ server *Server } | 73 | type compositor struct{ server *Server } | ||
| 74 | 74 | ||||
| 75 | func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) { | 75 | func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) { | ||
| 76 | - | state := &surface{server: c.server, bufferScale: 1} | 76 | + | state := &surface{server: c.server, bufferScale: 1, inputAll: true} |
| 77 | if w, err := wayland.NewSurface(r.Client(), r.Version(), id, state); err == nil { | 77 | if w, err := wayland.NewSurface(r.Client(), r.Version(), id, state); err == nil { | ||
| 78 | c.server.nextSurface++ | 78 | c.server.nextSurface++ | ||
| 79 | state.identity = c.server.nextSurface | 79 | state.identity = c.server.nextSurface | ||
| @@ -125,6 +125,7 @@ func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, par | |||||
| 125 | up.sub.pendingLayout = append(append([]childLayout(nil), up.sub.pendingLayout...), childLayout{child: state}) | 125 | up.sub.pendingLayout = append(append([]childLayout(nil), up.sub.pendingLayout...), childLayout{child: state}) | ||
| 126 | // The child follows its root window's output and scale. | 126 | // The child follows its root window's output and scale. | ||
| 127 | state.sendScale() | 127 | state.sendScale() | ||
| 128 | + | up.emitInput() | |||
| 128 | sub.OnDestroy = func() { | 129 | sub.OnDestroy = func() { | ||
| 129 | if state.sub.role != sub { | 130 | if state.sub.role != sub { | ||
| 130 | return | 131 | return | ||
| @@ -218,23 +219,40 @@ func (s subsurface) restack(r *wayland.Subsurface, sibling *wayland.Surface, abo | |||||
| 218 | p.sub.pendingLayout = list | 219 | p.sub.pendingLayout = list | ||
| 219 | } | 220 | } | ||
| 220 | 221 | ||||
| 221 | - | // region keeps the bounding box of the rectangles added: pointer | 222 | + | // region stores the exact union of added rectangles. |
| 222 | - | // confinement is the only user, and subtracted areas are ignored. | 223 | + | type region struct{ rects []ports.Rect } |
| 223 | - | type region struct{ box ports.Rect } | |||
| 224 | 224 | ||||
| 225 | func (*region) Destroy(*wayland.Region) {} | 225 | func (*region) Destroy(*wayland.Region) {} | ||
| 226 | func (g *region) Add(_ *wayland.Region, x, y, w, h int32) { | 226 | func (g *region) Add(_ *wayland.Region, x, y, w, h int32) { | ||
| 227 | + | if w > 0 && h > 0 { | |||
| 228 | + | g.rects = append(g.rects, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)}) | |||
| 229 | + | } | |||
| 230 | + | } | |||
| 231 | + | func (g *region) Subtract(_ *wayland.Region, x, y, w, h int32) { | |||
| 227 | if w <= 0 || h <= 0 { | 232 | if w <= 0 || h <= 0 { | ||
| 228 | return | 233 | return | ||
| 229 | } | 234 | } | ||
| 230 | - | r := ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)} | 235 | + | cut := ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)} |
| 231 | - | if g.box.W > 0 { | 236 | + | var out []ports.Rect |
| 232 | - | x0, y0 := min(g.box.X, r.X), min(g.box.Y, r.Y) | 237 | + | for _, r := range g.rects { |
| 233 | - | r = ports.Rect{X: x0, Y: y0, W: max(g.box.X+g.box.W, r.X+r.W) - x0, H: max(g.box.Y+g.box.H, r.Y+r.H) - y0} | 238 | + | i := intersectRect(r, cut) |
| 239 | + | if i.W == 0 { | |||
| 240 | + | out = append(out, r) | |||
| 241 | + | continue | |||
| 242 | + | } | |||
| 243 | + | for _, piece := range []ports.Rect{ | |||
| 244 | + | {X: r.X, Y: r.Y, W: r.W, H: i.Y - r.Y}, | |||
| 245 | + | {X: r.X, Y: i.Y + i.H, W: r.W, H: r.Y + r.H - i.Y - i.H}, | |||
| 246 | + | {X: r.X, Y: i.Y, W: i.X - r.X, H: i.H}, | |||
| 247 | + | {X: i.X + i.W, Y: i.Y, W: r.X + r.W - i.X - i.W, H: i.H}, | |||
| 248 | + | } { | |||
| 249 | + | if piece.W > 0 && piece.H > 0 { | |||
| 250 | + | out = append(out, piece) | |||
| 251 | + | } | |||
| 252 | + | } | |||
| 234 | } | 253 | } | ||
| 235 | - | g.box = r | 254 | + | g.rects = out |
| 236 | } | 255 | } | ||
| 237 | - | func (*region) Subtract(*wayland.Region, int32, int32, int32, int32) {} | |||
| 238 | 256 | ||||
| 239 | type shm struct{ server *Server } | 257 | type shm struct{ server *Server } | ||
| 240 | 258 | ||||
A internal/adapters/wayland/input_region.go +120 -0
| @@ -0,0 +1,120 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "slices" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |
| 7 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // intersectRect returns the overlap of two half-open rectangles. | |
| 11 | + | func intersectRect(a, b ports.Rect) ports.Rect { | |
| 12 | + | x, y := max(a.X, b.X), max(a.Y, b.Y) | |
| 13 | + | x2, y2 := min(a.X+a.W, b.X+b.W), min(a.Y+a.H, b.Y+b.H) | |
| 14 | + | if x2 <= x || y2 <= y { | |
| 15 | + | return ports.Rect{} | |
| 16 | + | } | |
| 17 | + | return ports.Rect{X: x, Y: y, W: x2 - x, H: y2 - y} | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | func (g *region) boxRect() ports.Rect { | |
| 21 | + | if len(g.rects) == 0 { | |
| 22 | + | return ports.Rect{} | |
| 23 | + | } | |
| 24 | + | x, y := g.rects[0].X, g.rects[0].Y | |
| 25 | + | x2, y2 := x+g.rects[0].W, y+g.rects[0].H | |
| 26 | + | for _, r := range g.rects[1:] { | |
| 27 | + | x = min(x, r.X) | |
| 28 | + | y = min(y, r.Y) | |
| 29 | + | x2 = max(x2, r.X+r.W) | |
| 30 | + | y2 = max(y2, r.Y+r.H) | |
| 31 | + | } | |
| 32 | + | return ports.Rect{X: x, Y: y, W: x2 - x, H: y2 - y} | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // effectiveInput includes all committed, bounded subsurface regions. | |
| 36 | + | func (s *surface) effectiveInput() (bool, []ports.Rect) { | |
| 37 | + | var rects []ports.Rect | |
| 38 | + | var visit func(*surface, int, int) | |
| 39 | + | visit = func(v *surface, x, y int) { | |
| 40 | + | if v.has { | |
| 41 | + | bounds := ports.Rect{W: v.content.LogicalW, H: v.content.LogicalH} | |
| 42 | + | if v.inputAll { | |
| 43 | + | if bounds.W > 0 && bounds.H > 0 { | |
| 44 | + | rects = append(rects, ports.Rect{X: x, Y: y, W: bounds.W, H: bounds.H}) | |
| 45 | + | } | |
| 46 | + | } else { | |
| 47 | + | for _, r := range v.inputRects { | |
| 48 | + | if r = intersectRect(r, bounds); r.W > 0 { | |
| 49 | + | r.X += x | |
| 50 | + | r.Y += y | |
| 51 | + | rects = append(rects, r) | |
| 52 | + | } | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | for _, item := range v.sub.layout { | |
| 57 | + | visit(item.child, x+item.x, y+item.y) | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | if s.inputAll && len(s.sub.children) == 0 { | |
| 61 | + | return true, nil | |
| 62 | + | } | |
| 63 | + | visit(s, 0, 0) | |
| 64 | + | if s.xdg != nil { | |
| 65 | + | for i := range rects { | |
| 66 | + | rects[i].X -= s.xdg.geometry.X | |
| 67 | + | rects[i].Y -= s.xdg.geometry.Y | |
| 68 | + | } | |
| 69 | + | } | |
| 70 | + | return false, rects | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // resetInputEmission makes the next mapping publish its current region again. | |
| 74 | + | func (s *surface) resetInputEmission() { | |
| 75 | + | s.sentInput = false | |
| 76 | + | s.lastInputAll = false | |
| 77 | + | s.lastInputRects = nil | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | func (s *surface) emitInput() { | |
| 81 | + | root := s.root() | |
| 82 | + | id := root.windowID() | |
| 83 | + | if id == 0 { | |
| 84 | + | return | |
| 85 | + | } | |
| 86 | + | all, rects := root.effectiveInput() | |
| 87 | + | if !root.sentInput && all { | |
| 88 | + | return | |
| 89 | + | } | |
| 90 | + | if root.sentInput && root.lastInputAll == all && slices.Equal(root.lastInputRects, rects) { | |
| 91 | + | return | |
| 92 | + | } | |
| 93 | + | root.sentInput = true | |
| 94 | + | root.lastInputAll = all | |
| 95 | + | root.lastInputRects = slices.Clone(rects) | |
| 96 | + | s.server.emit(ports.InputRegionChanged{ID: id, All: all, Rects: rects}) | |
| 97 | + | if c := s.server.constraints[root]; c != nil { | |
| 98 | + | if c.active { | |
| 99 | + | w := s.server.windows[id] | |
| 100 | + | if w == nil || !c.contains(w) { | |
| 101 | + | s.server.deactivateConstraint() | |
| 102 | + | } else { | |
| 103 | + | s.server.emitConstraint(c) | |
| 104 | + | } | |
| 105 | + | } else { | |
| 106 | + | s.server.updateConstraint() | |
| 107 | + | } | |
| 108 | + | } | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | func (s *surface) SetInputRegion(_ *wayland.Surface, reg *wayland.Region) { | |
| 112 | + | s.pendingInputSet = true | |
| 113 | + | s.pendingInputAll = reg == nil || reg.Resource == nil | |
| 114 | + | s.pendingInputRects = nil | |
| 115 | + | if !s.pendingInputAll { | |
| 116 | + | if g := s.server.regions[reg.Resource]; g != nil { | |
| 117 | + | s.pendingInputRects = slices.Clone(g.rects) | |
| 118 | + | } | |
| 119 | + | } | |
| 120 | + | } | |
| @@ -0,0 +1,120 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 7 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // intersectRect returns the overlap of two half-open rectangles. | |||
| 11 | + | func intersectRect(a, b ports.Rect) ports.Rect { | |||
| 12 | + | x, y := max(a.X, b.X), max(a.Y, b.Y) | |||
| 13 | + | x2, y2 := min(a.X+a.W, b.X+b.W), min(a.Y+a.H, b.Y+b.H) | |||
| 14 | + | if x2 <= x || y2 <= y { | |||
| 15 | + | return ports.Rect{} | |||
| 16 | + | } | |||
| 17 | + | return ports.Rect{X: x, Y: y, W: x2 - x, H: y2 - y} | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | func (g *region) boxRect() ports.Rect { | |||
| 21 | + | if len(g.rects) == 0 { | |||
| 22 | + | return ports.Rect{} | |||
| 23 | + | } | |||
| 24 | + | x, y := g.rects[0].X, g.rects[0].Y | |||
| 25 | + | x2, y2 := x+g.rects[0].W, y+g.rects[0].H | |||
| 26 | + | for _, r := range g.rects[1:] { | |||
| 27 | + | x = min(x, r.X) | |||
| 28 | + | y = min(y, r.Y) | |||
| 29 | + | x2 = max(x2, r.X+r.W) | |||
| 30 | + | y2 = max(y2, r.Y+r.H) | |||
| 31 | + | } | |||
| 32 | + | return ports.Rect{X: x, Y: y, W: x2 - x, H: y2 - y} | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // effectiveInput includes all committed, bounded subsurface regions. | |||
| 36 | + | func (s *surface) effectiveInput() (bool, []ports.Rect) { | |||
| 37 | + | var rects []ports.Rect | |||
| 38 | + | var visit func(*surface, int, int) | |||
| 39 | + | visit = func(v *surface, x, y int) { | |||
| 40 | + | if v.has { | |||
| 41 | + | bounds := ports.Rect{W: v.content.LogicalW, H: v.content.LogicalH} | |||
| 42 | + | if v.inputAll { | |||
| 43 | + | if bounds.W > 0 && bounds.H > 0 { | |||
| 44 | + | rects = append(rects, ports.Rect{X: x, Y: y, W: bounds.W, H: bounds.H}) | |||
| 45 | + | } | |||
| 46 | + | } else { | |||
| 47 | + | for _, r := range v.inputRects { | |||
| 48 | + | if r = intersectRect(r, bounds); r.W > 0 { | |||
| 49 | + | r.X += x | |||
| 50 | + | r.Y += y | |||
| 51 | + | rects = append(rects, r) | |||
| 52 | + | } | |||
| 53 | + | } | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | for _, item := range v.sub.layout { | |||
| 57 | + | visit(item.child, x+item.x, y+item.y) | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | if s.inputAll && len(s.sub.children) == 0 { | |||
| 61 | + | return true, nil | |||
| 62 | + | } | |||
| 63 | + | visit(s, 0, 0) | |||
| 64 | + | if s.xdg != nil { | |||
| 65 | + | for i := range rects { | |||
| 66 | + | rects[i].X -= s.xdg.geometry.X | |||
| 67 | + | rects[i].Y -= s.xdg.geometry.Y | |||
| 68 | + | } | |||
| 69 | + | } | |||
| 70 | + | return false, rects | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | // resetInputEmission makes the next mapping publish its current region again. | |||
| 74 | + | func (s *surface) resetInputEmission() { | |||
| 75 | + | s.sentInput = false | |||
| 76 | + | s.lastInputAll = false | |||
| 77 | + | s.lastInputRects = nil | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | func (s *surface) emitInput() { | |||
| 81 | + | root := s.root() | |||
| 82 | + | id := root.windowID() | |||
| 83 | + | if id == 0 { | |||
| 84 | + | return | |||
| 85 | + | } | |||
| 86 | + | all, rects := root.effectiveInput() | |||
| 87 | + | if !root.sentInput && all { | |||
| 88 | + | return | |||
| 89 | + | } | |||
| 90 | + | if root.sentInput && root.lastInputAll == all && slices.Equal(root.lastInputRects, rects) { | |||
| 91 | + | return | |||
| 92 | + | } | |||
| 93 | + | root.sentInput = true | |||
| 94 | + | root.lastInputAll = all | |||
| 95 | + | root.lastInputRects = slices.Clone(rects) | |||
| 96 | + | s.server.emit(ports.InputRegionChanged{ID: id, All: all, Rects: rects}) | |||
| 97 | + | if c := s.server.constraints[root]; c != nil { | |||
| 98 | + | if c.active { | |||
| 99 | + | w := s.server.windows[id] | |||
| 100 | + | if w == nil || !c.contains(w) { | |||
| 101 | + | s.server.deactivateConstraint() | |||
| 102 | + | } else { | |||
| 103 | + | s.server.emitConstraint(c) | |||
| 104 | + | } | |||
| 105 | + | } else { | |||
| 106 | + | s.server.updateConstraint() | |||
| 107 | + | } | |||
| 108 | + | } | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | func (s *surface) SetInputRegion(_ *wayland.Surface, reg *wayland.Region) { | |||
| 112 | + | s.pendingInputSet = true | |||
| 113 | + | s.pendingInputAll = reg == nil || reg.Resource == nil | |||
| 114 | + | s.pendingInputRects = nil | |||
| 115 | + | if !s.pendingInputAll { | |||
| 116 | + | if g := s.server.regions[reg.Resource]; g != nil { | |||
| 117 | + | s.pendingInputRects = slices.Clone(g.rects) | |||
| 118 | + | } | |||
| 119 | + | } | |||
| 120 | + | } | |||
A internal/adapters/wayland/input_region_test.go +327 -0
| @@ -0,0 +1,327 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "testing" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | "github.com/bnema/purego-libwayland/protocol/fifo" | |
| 10 | + | "github.com/bnema/purego-libwayland/protocol/pointerconstraints" | |
| 11 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 12 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 13 | + | "golang.org/x/sys/unix" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | func inputChanged(t *testing.T, events <-chan ports.ClientEvent) ports.InputRegionChanged { | |
| 17 | + | t.Helper() | |
| 18 | + | for { | |
| 19 | + | if v, ok := waitEvent(t, events, 2*time.Second).(ports.InputRegionChanged); ok { | |
| 20 | + | return v | |
| 21 | + | } | |
| 22 | + | } | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | func TestInputRegionCommitCopySubtractAndReset(t *testing.T) { | |
| 26 | + | s, events, _, dir := lifecycleServer(t) | |
| 27 | + | c := protocolClient(t, s, dir) | |
| 28 | + | w, surf, _ := surfaceMapper(t, c, events)() | |
| 29 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 30 | + | reg := c.AllocateID() | |
| 31 | + | registerProtocol(t, c, reg) | |
| 32 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, reg) | |
| 33 | + | requestProtocol(t, c, reg, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1)) | |
| 34 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, reg) | |
| 35 | + | requestProtocol(t, c, reg, wayland.RegionRequestSubtract, int32(0), int32(0), int32(1), int32(1)) | |
| 36 | + | requestProtocol(t, c, reg, wayland.RegionRequestDestroy) | |
| 37 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 38 | + | if got := inputChanged(t, events); got.ID != w.ID || len(got.Rects) != 1 || got.Rects[0] != (ports.Rect{W: 1, H: 1}) { | |
| 39 | + | t.Fatalf("copied region: %+v", got) | |
| 40 | + | } | |
| 41 | + | empty := c.AllocateID() | |
| 42 | + | registerProtocol(t, c, empty) | |
| 43 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, empty) | |
| 44 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, empty) | |
| 45 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 46 | + | if got := inputChanged(t, events); got.All || len(got.Rects) != 0 { | |
| 47 | + | t.Fatalf("empty region: %+v", got) | |
| 48 | + | } | |
| 49 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, uint32(0)) | |
| 50 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 51 | + | if got := inputChanged(t, events); !got.All { | |
| 52 | + | t.Fatalf("reset region: %+v", got) | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | func TestRegionSubtractSplits(t *testing.T) { | |
| 57 | + | g := ®ion{} | |
| 58 | + | g.Add(nil, 0, 0, 10, 10) | |
| 59 | + | g.Subtract(nil, 2, 2, 6, 6) | |
| 60 | + | if len(g.rects) != 4 || g.boxRect() != (ports.Rect{W: 10, H: 10}) { | |
| 61 | + | t.Fatalf("subtract: %+v", g.rects) | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | func TestSubsurfaceInputRegionOwnCommitAndOffset(t *testing.T) { | |
| 66 | + | s, events, _, dir := lifecycleServer(t) | |
| 67 | + | c := protocolClient(t, s, dir) | |
| 68 | + | w, root, _ := surfaceMapper(t, c, events)() | |
| 69 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 70 | + | subc := bindProtocol(t, c, "wl_subcompositor") | |
| 71 | + | child := c.AllocateID() | |
| 72 | + | registerProtocol(t, c, child) | |
| 73 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child) | |
| 74 | + | sub := c.AllocateID() | |
| 75 | + | registerProtocol(t, c, sub) | |
| 76 | + | requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root) | |
| 77 | + | // Desync so the child's own commits apply without a parent commit. | |
| 78 | + | requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetDesync) | |
| 79 | + | // surfaceMapper owns a one-pixel buffer; construct a second one for the child. | |
| 80 | + | shm := bindProtocol(t, c, "wl_shm") | |
| 81 | + | registerProtocol(t, c, shm) | |
| 82 | + | fd, err := unix.MemfdCreate("input-region-buffer", 0) | |
| 83 | + | if err != nil { | |
| 84 | + | t.Fatal(err) | |
| 85 | + | } | |
| 86 | + | defer unix.Close(fd) | |
| 87 | + | if err := unix.Ftruncate(fd, 64); err != nil { | |
| 88 | + | t.Fatal(err) | |
| 89 | + | } | |
| 90 | + | pool, buffer := c.AllocateID(), c.AllocateID() | |
| 91 | + | registerProtocol(t, c, pool) | |
| 92 | + | registerProtocol(t, c, buffer) | |
| 93 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(64)); err != nil { | |
| 94 | + | t.Fatal(err) | |
| 95 | + | } | |
| 96 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(4), int32(4), int32(16), uint32(0)) | |
| 97 | + | requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(3), int32(4)) | |
| 98 | + | requestProtocol(t, c, root, wayland.SurfaceRequestCommit) | |
| 99 | + | requestProtocol(t, c, child, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |
| 100 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |
| 101 | + | if err := c.Roundtrip(); err != nil { | |
| 102 | + | t.Fatal(err) | |
| 103 | + | } | |
| 104 | + | got := inputChanged(t, events) | |
| 105 | + | if len(got.Rects) == 1 { | |
| 106 | + | got = inputChanged(t, events) | |
| 107 | + | } | |
| 108 | + | if got.ID != w.ID || len(got.Rects) != 2 || got.Rects[1] != (ports.Rect{X: 3, Y: 4, W: 4, H: 4}) { | |
| 109 | + | t.Fatalf("default child input: %+v", got) | |
| 110 | + | } | |
| 111 | + | reg := c.AllocateID() | |
| 112 | + | registerProtocol(t, c, reg) | |
| 113 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, reg) | |
| 114 | + | requestProtocol(t, c, reg, wayland.RegionRequestAdd, int32(1), int32(1), int32(9), int32(9)) | |
| 115 | + | requestProtocol(t, c, child, wayland.SurfaceRequestSetInputRegion, reg) | |
| 116 | + | requestProtocol(t, c, reg, wayland.RegionRequestDestroy) | |
| 117 | + | if err := c.Roundtrip(); err != nil { | |
| 118 | + | t.Fatal(err) | |
| 119 | + | } | |
| 120 | + | // No parent commit is needed, but the child's own commit is required. | |
| 121 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |
| 122 | + | if err := c.Roundtrip(); err != nil { | |
| 123 | + | t.Fatal(err) | |
| 124 | + | } | |
| 125 | + | got = inputChanged(t, events) | |
| 126 | + | if got.ID != w.ID || len(got.Rects) != 2 || got.Rects[1] != (ports.Rect{X: 4, Y: 5, W: 3, H: 3}) { | |
| 127 | + | t.Fatalf("committed child input: %+v", got) | |
| 128 | + | } | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | func TestConstraintIntersectsInputRegion(t *testing.T) { | |
| 132 | + | s, events, commands, dir := lifecycleServer(t) | |
| 133 | + | c := protocolClient(t, s, dir) | |
| 134 | + | seat := bindProtocol(t, c, "wl_seat") | |
| 135 | + | registerProtocol(t, c, seat) | |
| 136 | + | pointer := c.AllocateID() | |
| 137 | + | registerProtocol(t, c, pointer) | |
| 138 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |
| 139 | + | w, surf, _ := surfaceMapper(t, c, events)() | |
| 140 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 141 | + | region := c.AllocateID() | |
| 142 | + | registerProtocol(t, c, region) | |
| 143 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |
| 144 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1)) | |
| 145 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, region) | |
| 146 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 147 | + | if err := c.Roundtrip(); err != nil { | |
| 148 | + | t.Fatal(err) | |
| 149 | + | } | |
| 150 | + | _ = inputChanged(t, events) | |
| 151 | + | constraints := bindProtocol(t, c, "zwp_pointer_constraints_v1") | |
| 152 | + | confine := c.AllocateID() | |
| 153 | + | registerProtocol(t, c, confine) | |
| 154 | + | requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |
| 155 | + | commands <- ports.FocusWindow{ID: w.ID} | |
| 156 | + | commands <- ports.PointerFocus{ID: w.ID} | |
| 157 | + | commands <- ports.PointerMotionTo{ID: w.ID, X: 0, Y: 0} | |
| 158 | + | if err := c.Roundtrip(); err != nil { | |
| 159 | + | t.Fatal(err) | |
| 160 | + | } | |
| 161 | + | if got := constrained(t, events); got.Rect != (ports.Rect{W: 1, H: 1}) { | |
| 162 | + | t.Fatalf("constraint intersection: %+v", got) | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | func TestInputRegionResentAfterRemap(t *testing.T) { | |
| 167 | + | s, events, _, dir := lifecycleServer(t) | |
| 168 | + | c := protocolClient(t, s, dir) | |
| 169 | + | w, surf, xdg := surfaceMapper(t, c, events)() | |
| 170 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 171 | + | region := c.AllocateID() | |
| 172 | + | registerProtocol(t, c, region) | |
| 173 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |
| 174 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1)) | |
| 175 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, region) | |
| 176 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 177 | + | if err := c.Roundtrip(); err != nil { | |
| 178 | + | t.Fatal(err) | |
| 179 | + | } | |
| 180 | + | if got := inputChanged(t, events); got.ID != w.ID || got.All { | |
| 181 | + | t.Fatalf("initial input: %+v", got) | |
| 182 | + | } | |
| 183 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0)) | |
| 184 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 185 | + | if err := c.Roundtrip(); err != nil { | |
| 186 | + | t.Fatal(err) | |
| 187 | + | } | |
| 188 | + | unmapped(t, events, w.ID) | |
| 189 | + | // Remapping requires a new initial configure and an ack before attaching. | |
| 190 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 191 | + | if err := c.Roundtrip(); err != nil { | |
| 192 | + | t.Fatal(err) | |
| 193 | + | } | |
| 194 | + | var serial uint32 | |
| 195 | + | if !s.display.Do(func() { | |
| 196 | + | for _, v := range s.surfaces { | |
| 197 | + | if v.xdg != nil && v.xdg.window != nil && v.xdg.window.id == w.ID && len(v.xdg.serials) > 0 { | |
| 198 | + | serial = v.xdg.serials[len(v.xdg.serials)-1] | |
| 199 | + | } | |
| 200 | + | } | |
| 201 | + | }) { | |
| 202 | + | t.Fatal("server stopped") | |
| 203 | + | } | |
| 204 | + | if serial == 0 { | |
| 205 | + | t.Fatal("no remap configure") | |
| 206 | + | } | |
| 207 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial) | |
| 208 | + | buffer := shmBuffer(t, c) | |
| 209 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |
| 210 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 211 | + | if err := c.Roundtrip(); err != nil { | |
| 212 | + | t.Fatal(err) | |
| 213 | + | } | |
| 214 | + | if got := mapped(t, events, 2*time.Second); got.ID != w.ID { | |
| 215 | + | t.Fatalf("remapped: %+v", got) | |
| 216 | + | } | |
| 217 | + | if got := inputChanged(t, events); got.ID != w.ID || got.All || len(got.Rects) != 1 || got.Rects[0] != (ports.Rect{W: 1, H: 1}) { | |
| 218 | + | t.Fatalf("remap input: %+v", got) | |
| 219 | + | } | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | func TestRegionOverlappingAddsSubtractUnion(t *testing.T) { | |
| 223 | + | g := ®ion{} | |
| 224 | + | g.Add(nil, 0, 0, 6, 6) | |
| 225 | + | g.Add(nil, 2, 2, 6, 6) | |
| 226 | + | g.Subtract(nil, 3, 3, 2, 2) | |
| 227 | + | for _, r := range g.rects { | |
| 228 | + | if intersectRect(r, ports.Rect{X: 3, Y: 3, W: 2, H: 2}).W != 0 { | |
| 229 | + | t.Fatalf("subtracted overlap remains: %+v", g.rects) | |
| 230 | + | } | |
| 231 | + | } | |
| 232 | + | if g.boxRect() != (ports.Rect{W: 8, H: 8}) { | |
| 233 | + | t.Fatalf("outer region changed: %+v", g.rects) | |
| 234 | + | } | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | func TestInputRegionDeactivatesConstraint(t *testing.T) { | |
| 238 | + | for _, kind := range []string{"confine", "lock"} { | |
| 239 | + | for _, persistent := range []bool{false, true} { | |
| 240 | + | t.Run(fmt.Sprintf("%s/persistent=%t", kind, persistent), func(t *testing.T) { | |
| 241 | + | s, events, commands, dir := lifecycleServer(t) | |
| 242 | + | c := protocolClient(t, s, dir) | |
| 243 | + | seat := bindProtocol(t, c, "wl_seat") | |
| 244 | + | registerProtocol(t, c, seat) | |
| 245 | + | pointer := c.AllocateID() | |
| 246 | + | registerProtocol(t, c, pointer) | |
| 247 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |
| 248 | + | w, surf, _ := surfaceMapper(t, c, events)() | |
| 249 | + | manager := bindProtocol(t, c, "zwp_pointer_constraints_v1") | |
| 250 | + | id := c.AllocateID() | |
| 251 | + | notices := logProxy(t, c, id) | |
| 252 | + | lifetime := uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot) | |
| 253 | + | if persistent { | |
| 254 | + | lifetime = uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent) | |
| 255 | + | } | |
| 256 | + | if kind == "lock" { | |
| 257 | + | requestProtocol(t, c, manager, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, id, surf, pointer, uint32(0), lifetime) | |
| 258 | + | } else { | |
| 259 | + | requestProtocol(t, c, manager, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, id, surf, pointer, uint32(0), lifetime) | |
| 260 | + | } | |
| 261 | + | commands <- ports.PointerFocus{ID: w.ID} | |
| 262 | + | commands <- ports.FocusWindow{ID: w.ID} | |
| 263 | + | if ev := next(t, c, notices); ev[0] != 0 { | |
| 264 | + | t.Fatalf("activation: %v", ev) | |
| 265 | + | } | |
| 266 | + | if got := constrained(t, events); got.ID != w.ID { | |
| 267 | + | t.Fatalf("constraint: %+v", got) | |
| 268 | + | } | |
| 269 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 270 | + | region := c.AllocateID() | |
| 271 | + | registerProtocol(t, c, region) | |
| 272 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |
| 273 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, region) | |
| 274 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 275 | + | if ev := next(t, c, notices); ev[0] != 1 { | |
| 276 | + | t.Fatalf("deactivation: %v", ev) | |
| 277 | + | } | |
| 278 | + | if got := constrained(t, events); got.ID != 0 { | |
| 279 | + | t.Fatalf("constraint retained: %+v", got) | |
| 280 | + | } | |
| 281 | + | if err := c.Roundtrip(); err != nil { | |
| 282 | + | t.Fatal(err) | |
| 283 | + | } | |
| 284 | + | select { | |
| 285 | + | case ev := <-notices: | |
| 286 | + | t.Fatalf("unexpected reactivation: %v", ev) | |
| 287 | + | default: | |
| 288 | + | } | |
| 289 | + | }) | |
| 290 | + | } | |
| 291 | + | } | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | func TestFifoQueuedInputRegionsApplyInOrder(t *testing.T) { | |
| 295 | + | s, events, _, dir := lifecycleServer(t) | |
| 296 | + | c := protocolClient(t, s, dir) | |
| 297 | + | w, surf, _ := surfaceMapper(t, c, events)() | |
| 298 | + | manager := bindProtocol(t, c, "wp_fifo_manager_v1") | |
| 299 | + | f := c.AllocateID() | |
| 300 | + | registerProtocol(t, c, f) | |
| 301 | + | requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf) | |
| 302 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 303 | + | region := c.AllocateID() | |
| 304 | + | registerProtocol(t, c, region) | |
| 305 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |
| 306 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1)) | |
| 307 | + | // First commit sets a barrier; the next two wait behind it. | |
| 308 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier) | |
| 309 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 310 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier) | |
| 311 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, region) | |
| 312 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 313 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier) | |
| 314 | + | empty := c.AllocateID() | |
| 315 | + | registerProtocol(t, c, empty) | |
| 316 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, empty) | |
| 317 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, empty) | |
| 318 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 319 | + | if err := c.Roundtrip(); err != nil { | |
| 320 | + | t.Fatal(err) | |
| 321 | + | } | |
| 322 | + | first := inputChanged(t, events) | |
| 323 | + | second := inputChanged(t, events) | |
| 324 | + | if first.ID != w.ID || len(first.Rects) != 1 || first.Rects[0] != (ports.Rect{W: 1, H: 1}) || second.ID != w.ID || second.All || len(second.Rects) != 0 { | |
| 325 | + | t.Fatalf("queued input order: first %+v, second %+v", first, second) | |
| 326 | + | } | |
| 327 | + | } | |
| @@ -0,0 +1,327 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "testing" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | "github.com/bnema/purego-libwayland/protocol/fifo" | |||
| 10 | + | "github.com/bnema/purego-libwayland/protocol/pointerconstraints" | |||
| 11 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 12 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |||
| 13 | + | "golang.org/x/sys/unix" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | func inputChanged(t *testing.T, events <-chan ports.ClientEvent) ports.InputRegionChanged { | |||
| 17 | + | t.Helper() | |||
| 18 | + | for { | |||
| 19 | + | if v, ok := waitEvent(t, events, 2*time.Second).(ports.InputRegionChanged); ok { | |||
| 20 | + | return v | |||
| 21 | + | } | |||
| 22 | + | } | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | func TestInputRegionCommitCopySubtractAndReset(t *testing.T) { | |||
| 26 | + | s, events, _, dir := lifecycleServer(t) | |||
| 27 | + | c := protocolClient(t, s, dir) | |||
| 28 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 29 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 30 | + | reg := c.AllocateID() | |||
| 31 | + | registerProtocol(t, c, reg) | |||
| 32 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, reg) | |||
| 33 | + | requestProtocol(t, c, reg, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1)) | |||
| 34 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, reg) | |||
| 35 | + | requestProtocol(t, c, reg, wayland.RegionRequestSubtract, int32(0), int32(0), int32(1), int32(1)) | |||
| 36 | + | requestProtocol(t, c, reg, wayland.RegionRequestDestroy) | |||
| 37 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 38 | + | if got := inputChanged(t, events); got.ID != w.ID || len(got.Rects) != 1 || got.Rects[0] != (ports.Rect{W: 1, H: 1}) { | |||
| 39 | + | t.Fatalf("copied region: %+v", got) | |||
| 40 | + | } | |||
| 41 | + | empty := c.AllocateID() | |||
| 42 | + | registerProtocol(t, c, empty) | |||
| 43 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, empty) | |||
| 44 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, empty) | |||
| 45 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 46 | + | if got := inputChanged(t, events); got.All || len(got.Rects) != 0 { | |||
| 47 | + | t.Fatalf("empty region: %+v", got) | |||
| 48 | + | } | |||
| 49 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, uint32(0)) | |||
| 50 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 51 | + | if got := inputChanged(t, events); !got.All { | |||
| 52 | + | t.Fatalf("reset region: %+v", got) | |||
| 53 | + | } | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | func TestRegionSubtractSplits(t *testing.T) { | |||
| 57 | + | g := ®ion{} | |||
| 58 | + | g.Add(nil, 0, 0, 10, 10) | |||
| 59 | + | g.Subtract(nil, 2, 2, 6, 6) | |||
| 60 | + | if len(g.rects) != 4 || g.boxRect() != (ports.Rect{W: 10, H: 10}) { | |||
| 61 | + | t.Fatalf("subtract: %+v", g.rects) | |||
| 62 | + | } | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | func TestSubsurfaceInputRegionOwnCommitAndOffset(t *testing.T) { | |||
| 66 | + | s, events, _, dir := lifecycleServer(t) | |||
| 67 | + | c := protocolClient(t, s, dir) | |||
| 68 | + | w, root, _ := surfaceMapper(t, c, events)() | |||
| 69 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 70 | + | subc := bindProtocol(t, c, "wl_subcompositor") | |||
| 71 | + | child := c.AllocateID() | |||
| 72 | + | registerProtocol(t, c, child) | |||
| 73 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child) | |||
| 74 | + | sub := c.AllocateID() | |||
| 75 | + | registerProtocol(t, c, sub) | |||
| 76 | + | requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root) | |||
| 77 | + | // Desync so the child's own commits apply without a parent commit. | |||
| 78 | + | requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetDesync) | |||
| 79 | + | // surfaceMapper owns a one-pixel buffer; construct a second one for the child. | |||
| 80 | + | shm := bindProtocol(t, c, "wl_shm") | |||
| 81 | + | registerProtocol(t, c, shm) | |||
| 82 | + | fd, err := unix.MemfdCreate("input-region-buffer", 0) | |||
| 83 | + | if err != nil { | |||
| 84 | + | t.Fatal(err) | |||
| 85 | + | } | |||
| 86 | + | defer unix.Close(fd) | |||
| 87 | + | if err := unix.Ftruncate(fd, 64); err != nil { | |||
| 88 | + | t.Fatal(err) | |||
| 89 | + | } | |||
| 90 | + | pool, buffer := c.AllocateID(), c.AllocateID() | |||
| 91 | + | registerProtocol(t, c, pool) | |||
| 92 | + | registerProtocol(t, c, buffer) | |||
| 93 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(64)); err != nil { | |||
| 94 | + | t.Fatal(err) | |||
| 95 | + | } | |||
| 96 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(4), int32(4), int32(16), uint32(0)) | |||
| 97 | + | requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(3), int32(4)) | |||
| 98 | + | requestProtocol(t, c, root, wayland.SurfaceRequestCommit) | |||
| 99 | + | requestProtocol(t, c, child, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |||
| 100 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |||
| 101 | + | if err := c.Roundtrip(); err != nil { | |||
| 102 | + | t.Fatal(err) | |||
| 103 | + | } | |||
| 104 | + | got := inputChanged(t, events) | |||
| 105 | + | if len(got.Rects) == 1 { | |||
| 106 | + | got = inputChanged(t, events) | |||
| 107 | + | } | |||
| 108 | + | if got.ID != w.ID || len(got.Rects) != 2 || got.Rects[1] != (ports.Rect{X: 3, Y: 4, W: 4, H: 4}) { | |||
| 109 | + | t.Fatalf("default child input: %+v", got) | |||
| 110 | + | } | |||
| 111 | + | reg := c.AllocateID() | |||
| 112 | + | registerProtocol(t, c, reg) | |||
| 113 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, reg) | |||
| 114 | + | requestProtocol(t, c, reg, wayland.RegionRequestAdd, int32(1), int32(1), int32(9), int32(9)) | |||
| 115 | + | requestProtocol(t, c, child, wayland.SurfaceRequestSetInputRegion, reg) | |||
| 116 | + | requestProtocol(t, c, reg, wayland.RegionRequestDestroy) | |||
| 117 | + | if err := c.Roundtrip(); err != nil { | |||
| 118 | + | t.Fatal(err) | |||
| 119 | + | } | |||
| 120 | + | // No parent commit is needed, but the child's own commit is required. | |||
| 121 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |||
| 122 | + | if err := c.Roundtrip(); err != nil { | |||
| 123 | + | t.Fatal(err) | |||
| 124 | + | } | |||
| 125 | + | got = inputChanged(t, events) | |||
| 126 | + | if got.ID != w.ID || len(got.Rects) != 2 || got.Rects[1] != (ports.Rect{X: 4, Y: 5, W: 3, H: 3}) { | |||
| 127 | + | t.Fatalf("committed child input: %+v", got) | |||
| 128 | + | } | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | func TestConstraintIntersectsInputRegion(t *testing.T) { | |||
| 132 | + | s, events, commands, dir := lifecycleServer(t) | |||
| 133 | + | c := protocolClient(t, s, dir) | |||
| 134 | + | seat := bindProtocol(t, c, "wl_seat") | |||
| 135 | + | registerProtocol(t, c, seat) | |||
| 136 | + | pointer := c.AllocateID() | |||
| 137 | + | registerProtocol(t, c, pointer) | |||
| 138 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 139 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 140 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 141 | + | region := c.AllocateID() | |||
| 142 | + | registerProtocol(t, c, region) | |||
| 143 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |||
| 144 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1)) | |||
| 145 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, region) | |||
| 146 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 147 | + | if err := c.Roundtrip(); err != nil { | |||
| 148 | + | t.Fatal(err) | |||
| 149 | + | } | |||
| 150 | + | _ = inputChanged(t, events) | |||
| 151 | + | constraints := bindProtocol(t, c, "zwp_pointer_constraints_v1") | |||
| 152 | + | confine := c.AllocateID() | |||
| 153 | + | registerProtocol(t, c, confine) | |||
| 154 | + | requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |||
| 155 | + | commands <- ports.FocusWindow{ID: w.ID} | |||
| 156 | + | commands <- ports.PointerFocus{ID: w.ID} | |||
| 157 | + | commands <- ports.PointerMotionTo{ID: w.ID, X: 0, Y: 0} | |||
| 158 | + | if err := c.Roundtrip(); err != nil { | |||
| 159 | + | t.Fatal(err) | |||
| 160 | + | } | |||
| 161 | + | if got := constrained(t, events); got.Rect != (ports.Rect{W: 1, H: 1}) { | |||
| 162 | + | t.Fatalf("constraint intersection: %+v", got) | |||
| 163 | + | } | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | func TestInputRegionResentAfterRemap(t *testing.T) { | |||
| 167 | + | s, events, _, dir := lifecycleServer(t) | |||
| 168 | + | c := protocolClient(t, s, dir) | |||
| 169 | + | w, surf, xdg := surfaceMapper(t, c, events)() | |||
| 170 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 171 | + | region := c.AllocateID() | |||
| 172 | + | registerProtocol(t, c, region) | |||
| 173 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |||
| 174 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1)) | |||
| 175 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, region) | |||
| 176 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 177 | + | if err := c.Roundtrip(); err != nil { | |||
| 178 | + | t.Fatal(err) | |||
| 179 | + | } | |||
| 180 | + | if got := inputChanged(t, events); got.ID != w.ID || got.All { | |||
| 181 | + | t.Fatalf("initial input: %+v", got) | |||
| 182 | + | } | |||
| 183 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0)) | |||
| 184 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 185 | + | if err := c.Roundtrip(); err != nil { | |||
| 186 | + | t.Fatal(err) | |||
| 187 | + | } | |||
| 188 | + | unmapped(t, events, w.ID) | |||
| 189 | + | // Remapping requires a new initial configure and an ack before attaching. | |||
| 190 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 191 | + | if err := c.Roundtrip(); err != nil { | |||
| 192 | + | t.Fatal(err) | |||
| 193 | + | } | |||
| 194 | + | var serial uint32 | |||
| 195 | + | if !s.display.Do(func() { | |||
| 196 | + | for _, v := range s.surfaces { | |||
| 197 | + | if v.xdg != nil && v.xdg.window != nil && v.xdg.window.id == w.ID && len(v.xdg.serials) > 0 { | |||
| 198 | + | serial = v.xdg.serials[len(v.xdg.serials)-1] | |||
| 199 | + | } | |||
| 200 | + | } | |||
| 201 | + | }) { | |||
| 202 | + | t.Fatal("server stopped") | |||
| 203 | + | } | |||
| 204 | + | if serial == 0 { | |||
| 205 | + | t.Fatal("no remap configure") | |||
| 206 | + | } | |||
| 207 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial) | |||
| 208 | + | buffer := shmBuffer(t, c) | |||
| 209 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |||
| 210 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 211 | + | if err := c.Roundtrip(); err != nil { | |||
| 212 | + | t.Fatal(err) | |||
| 213 | + | } | |||
| 214 | + | if got := mapped(t, events, 2*time.Second); got.ID != w.ID { | |||
| 215 | + | t.Fatalf("remapped: %+v", got) | |||
| 216 | + | } | |||
| 217 | + | if got := inputChanged(t, events); got.ID != w.ID || got.All || len(got.Rects) != 1 || got.Rects[0] != (ports.Rect{W: 1, H: 1}) { | |||
| 218 | + | t.Fatalf("remap input: %+v", got) | |||
| 219 | + | } | |||
| 220 | + | } | |||
| 221 | + | ||||
| 222 | + | func TestRegionOverlappingAddsSubtractUnion(t *testing.T) { | |||
| 223 | + | g := ®ion{} | |||
| 224 | + | g.Add(nil, 0, 0, 6, 6) | |||
| 225 | + | g.Add(nil, 2, 2, 6, 6) | |||
| 226 | + | g.Subtract(nil, 3, 3, 2, 2) | |||
| 227 | + | for _, r := range g.rects { | |||
| 228 | + | if intersectRect(r, ports.Rect{X: 3, Y: 3, W: 2, H: 2}).W != 0 { | |||
| 229 | + | t.Fatalf("subtracted overlap remains: %+v", g.rects) | |||
| 230 | + | } | |||
| 231 | + | } | |||
| 232 | + | if g.boxRect() != (ports.Rect{W: 8, H: 8}) { | |||
| 233 | + | t.Fatalf("outer region changed: %+v", g.rects) | |||
| 234 | + | } | |||
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | func TestInputRegionDeactivatesConstraint(t *testing.T) { | |||
| 238 | + | for _, kind := range []string{"confine", "lock"} { | |||
| 239 | + | for _, persistent := range []bool{false, true} { | |||
| 240 | + | t.Run(fmt.Sprintf("%s/persistent=%t", kind, persistent), func(t *testing.T) { | |||
| 241 | + | s, events, commands, dir := lifecycleServer(t) | |||
| 242 | + | c := protocolClient(t, s, dir) | |||
| 243 | + | seat := bindProtocol(t, c, "wl_seat") | |||
| 244 | + | registerProtocol(t, c, seat) | |||
| 245 | + | pointer := c.AllocateID() | |||
| 246 | + | registerProtocol(t, c, pointer) | |||
| 247 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 248 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 249 | + | manager := bindProtocol(t, c, "zwp_pointer_constraints_v1") | |||
| 250 | + | id := c.AllocateID() | |||
| 251 | + | notices := logProxy(t, c, id) | |||
| 252 | + | lifetime := uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot) | |||
| 253 | + | if persistent { | |||
| 254 | + | lifetime = uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent) | |||
| 255 | + | } | |||
| 256 | + | if kind == "lock" { | |||
| 257 | + | requestProtocol(t, c, manager, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, id, surf, pointer, uint32(0), lifetime) | |||
| 258 | + | } else { | |||
| 259 | + | requestProtocol(t, c, manager, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, id, surf, pointer, uint32(0), lifetime) | |||
| 260 | + | } | |||
| 261 | + | commands <- ports.PointerFocus{ID: w.ID} | |||
| 262 | + | commands <- ports.FocusWindow{ID: w.ID} | |||
| 263 | + | if ev := next(t, c, notices); ev[0] != 0 { | |||
| 264 | + | t.Fatalf("activation: %v", ev) | |||
| 265 | + | } | |||
| 266 | + | if got := constrained(t, events); got.ID != w.ID { | |||
| 267 | + | t.Fatalf("constraint: %+v", got) | |||
| 268 | + | } | |||
| 269 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 270 | + | region := c.AllocateID() | |||
| 271 | + | registerProtocol(t, c, region) | |||
| 272 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |||
| 273 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, region) | |||
| 274 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 275 | + | if ev := next(t, c, notices); ev[0] != 1 { | |||
| 276 | + | t.Fatalf("deactivation: %v", ev) | |||
| 277 | + | } | |||
| 278 | + | if got := constrained(t, events); got.ID != 0 { | |||
| 279 | + | t.Fatalf("constraint retained: %+v", got) | |||
| 280 | + | } | |||
| 281 | + | if err := c.Roundtrip(); err != nil { | |||
| 282 | + | t.Fatal(err) | |||
| 283 | + | } | |||
| 284 | + | select { | |||
| 285 | + | case ev := <-notices: | |||
| 286 | + | t.Fatalf("unexpected reactivation: %v", ev) | |||
| 287 | + | default: | |||
| 288 | + | } | |||
| 289 | + | }) | |||
| 290 | + | } | |||
| 291 | + | } | |||
| 292 | + | } | |||
| 293 | + | ||||
| 294 | + | func TestFifoQueuedInputRegionsApplyInOrder(t *testing.T) { | |||
| 295 | + | s, events, _, dir := lifecycleServer(t) | |||
| 296 | + | c := protocolClient(t, s, dir) | |||
| 297 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 298 | + | manager := bindProtocol(t, c, "wp_fifo_manager_v1") | |||
| 299 | + | f := c.AllocateID() | |||
| 300 | + | registerProtocol(t, c, f) | |||
| 301 | + | requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf) | |||
| 302 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 303 | + | region := c.AllocateID() | |||
| 304 | + | registerProtocol(t, c, region) | |||
| 305 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |||
| 306 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1)) | |||
| 307 | + | // First commit sets a barrier; the next two wait behind it. | |||
| 308 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier) | |||
| 309 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 310 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier) | |||
| 311 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, region) | |||
| 312 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 313 | + | requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier) | |||
| 314 | + | empty := c.AllocateID() | |||
| 315 | + | registerProtocol(t, c, empty) | |||
| 316 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, empty) | |||
| 317 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, empty) | |||
| 318 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 319 | + | if err := c.Roundtrip(); err != nil { | |||
| 320 | + | t.Fatal(err) | |||
| 321 | + | } | |||
| 322 | + | first := inputChanged(t, events) | |||
| 323 | + | second := inputChanged(t, events) | |||
| 324 | + | if first.ID != w.ID || len(first.Rects) != 1 || first.Rects[0] != (ports.Rect{W: 1, H: 1}) || second.ID != w.ID || second.All || len(second.Rects) != 0 { | |||
| 325 | + | t.Fatalf("queued input order: first %+v, second %+v", first, second) | |||
| 326 | + | } | |||
| 327 | + | } | |||
M internal/adapters/wayland/layer.go +1 -0
| @@ -163,6 +163,7 @@ func (l *layerSurface) unmap() { | |||
| 163 | 163 | s.changePointerFocus(0, 0, 0) | |
| 164 | 164 | } | |
| 165 | 165 | l.mapped = false | |
| 166 | + | l.surface.resetInputEmission() | |
| 166 | 167 | l.configured = false | |
| 167 | 168 | l.acked = false | |
| 168 | 169 | l.serials = nil | |
| @@ -163,6 +163,7 @@ func (l *layerSurface) unmap() { | |||||
| 163 | s.changePointerFocus(0, 0, 0) | 163 | s.changePointerFocus(0, 0, 0) | ||
| 164 | } | 164 | } | ||
| 165 | l.mapped = false | 165 | l.mapped = false | ||
| 166 | + | l.surface.resetInputEmission() | |||
| 166 | l.configured = false | 167 | l.configured = false | ||
| 167 | l.acked = false | 168 | l.acked = false | ||
| 168 | l.serials = nil | 169 | l.serials = nil | ||
M internal/adapters/wayland/surface.go +15 -5
| @@ -45,10 +45,14 @@ type surface struct { | |||
| 45 | 45 | scale float64 | |
| 46 | 46 | // content is the last content built from this surface's own buffer; | |
| 47 | 47 | // has is false while no buffer is attached. | |
| 48 | - | content ports.SurfaceContent | |
| 49 | - | has bool | |
| 50 | - | identity uint64 | |
| 51 | - | version uint64 | |
| 48 | + | inputAll, pendingInputAll, pendingInputSet bool | |
| 49 | + | inputRects, pendingInputRects []ports.Rect | |
| 50 | + | sentInput, lastInputAll bool | |
| 51 | + | lastInputRects []ports.Rect | |
| 52 | + | content ports.SurfaceContent | |
| 53 | + | has bool | |
| 54 | + | identity uint64 | |
| 55 | + | version uint64 | |
| 52 | 56 | // cachedTree is never modified once returned to emitContent. | |
| 53 | 57 | cachedTree []ports.Subsurface | |
| 54 | 58 | treeDirty bool | |
| @@ -233,6 +237,7 @@ func (s *surface) detach() { | |||
| 233 | 237 | s.dropQueue() | |
| 234 | 238 | s.flushDesync() | |
| 235 | 239 | p.redraw() | |
| 240 | + | p.emitInput() | |
| 236 | 241 | s.sendScale() | |
| 237 | 242 | } | |
| 238 | 243 | ||
| @@ -359,6 +364,11 @@ func (s *surface) applyCommit() { | |||
| 359 | 364 | s.attached = false | |
| 360 | 365 | } | |
| 361 | 366 | s.applySync(cs) | |
| 367 | + | if s.pendingInputSet { | |
| 368 | + | s.inputAll, s.inputRects = s.pendingInputAll, s.pendingInputRects | |
| 369 | + | s.pendingInputRects = nil | |
| 370 | + | s.pendingInputSet = false | |
| 371 | + | } | |
| 362 | 372 | if len(s.callbacks) > 0 { | |
| 363 | 373 | s.server.queueFrames(s.server.frameOutput(s), s.callbacks) | |
| 364 | 374 | } | |
| @@ -423,6 +433,7 @@ func (s *surface) applyCommit() { | |||
| 423 | 433 | s.xdg.geometry, geometry = s.xdg.pendingGeometry, true | |
| 424 | 434 | } | |
| 425 | 435 | s.commitConstraint(geometry) | |
| 436 | + | s.emitInput() | |
| 426 | 437 | if s.xdg != nil && s.xdg.window != nil { | |
| 427 | 438 | s.xdg.window.afterCommit() | |
| 428 | 439 | } | |
| @@ -500,7 +511,6 @@ func (s *surface) commitDamage(fresh, resized bool, bw, bh int) { | |||
| 500 | 511 | s.committed = d | |
| 501 | 512 | } | |
| 502 | 513 | func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {} | |
| 503 | - | func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {} | |
| 504 | 514 | func (*surface) SetBufferTransform(*wayland.Surface, int32) {} | |
| 505 | 515 | func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | |
| 506 | 516 | if v < 1 { | |
| @@ -45,10 +45,14 @@ type surface struct { | |||||
| 45 | scale float64 | 45 | scale float64 | ||
| 46 | // content is the last content built from this surface's own buffer; | 46 | // content is the last content built from this surface's own buffer; | ||
| 47 | // has is false while no buffer is attached. | 47 | // has is false while no buffer is attached. | ||
| 48 | - | content ports.SurfaceContent | 48 | + | inputAll, pendingInputAll, pendingInputSet bool |
| 49 | - | has bool | 49 | + | inputRects, pendingInputRects []ports.Rect |
| 50 | - | identity uint64 | 50 | + | sentInput, lastInputAll bool |
| 51 | - | version uint64 | 51 | + | lastInputRects []ports.Rect |
| 52 | + | content ports.SurfaceContent | |||
| 53 | + | has bool | |||
| 54 | + | identity uint64 | |||
| 55 | + | version uint64 | |||
| 52 | // cachedTree is never modified once returned to emitContent. | 56 | // cachedTree is never modified once returned to emitContent. | ||
| 53 | cachedTree []ports.Subsurface | 57 | cachedTree []ports.Subsurface | ||
| 54 | treeDirty bool | 58 | treeDirty bool | ||
| @@ -233,6 +237,7 @@ func (s *surface) detach() { | |||||
| 233 | s.dropQueue() | 237 | s.dropQueue() | ||
| 234 | s.flushDesync() | 238 | s.flushDesync() | ||
| 235 | p.redraw() | 239 | p.redraw() | ||
| 240 | + | p.emitInput() | |||
| 236 | s.sendScale() | 241 | s.sendScale() | ||
| 237 | } | 242 | } | ||
| 238 | 243 | ||||
| @@ -359,6 +364,11 @@ func (s *surface) applyCommit() { | |||||
| 359 | s.attached = false | 364 | s.attached = false | ||
| 360 | } | 365 | } | ||
| 361 | s.applySync(cs) | 366 | s.applySync(cs) | ||
| 367 | + | if s.pendingInputSet { | |||
| 368 | + | s.inputAll, s.inputRects = s.pendingInputAll, s.pendingInputRects | |||
| 369 | + | s.pendingInputRects = nil | |||
| 370 | + | s.pendingInputSet = false | |||
| 371 | + | } | |||
| 362 | if len(s.callbacks) > 0 { | 372 | if len(s.callbacks) > 0 { | ||
| 363 | s.server.queueFrames(s.server.frameOutput(s), s.callbacks) | 373 | s.server.queueFrames(s.server.frameOutput(s), s.callbacks) | ||
| 364 | } | 374 | } | ||
| @@ -423,6 +433,7 @@ func (s *surface) applyCommit() { | |||||
| 423 | s.xdg.geometry, geometry = s.xdg.pendingGeometry, true | 433 | s.xdg.geometry, geometry = s.xdg.pendingGeometry, true | ||
| 424 | } | 434 | } | ||
| 425 | s.commitConstraint(geometry) | 435 | s.commitConstraint(geometry) | ||
| 436 | + | s.emitInput() | |||
| 426 | if s.xdg != nil && s.xdg.window != nil { | 437 | if s.xdg != nil && s.xdg.window != nil { | ||
| 427 | s.xdg.window.afterCommit() | 438 | s.xdg.window.afterCommit() | ||
| 428 | } | 439 | } | ||
| @@ -500,7 +511,6 @@ func (s *surface) commitDamage(fresh, resized bool, bw, bh int) { | |||||
| 500 | s.committed = d | 511 | s.committed = d | ||
| 501 | } | 512 | } | ||
| 502 | func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {} | 513 | func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {} | ||
| 503 | - | func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {} | |||
| 504 | func (*surface) SetBufferTransform(*wayland.Surface, int32) {} | 514 | func (*surface) SetBufferTransform(*wayland.Surface, int32) {} | ||
| 505 | func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | 515 | func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | ||
| 506 | if v < 1 { | 516 | if v < 1 { | ||
M internal/adapters/wayland/xdg.go +1 -0
| @@ -178,6 +178,7 @@ func (w *window) unmap() { | |||
| 178 | 178 | return | |
| 179 | 179 | } | |
| 180 | 180 | w.mapped = false | |
| 181 | + | w.xdg.surface.resetInputEmission() | |
| 181 | 182 | w.xdg.configured = false | |
| 182 | 183 | w.xdg.acked = false | |
| 183 | 184 | w.xdg.serials = nil | |
| @@ -178,6 +178,7 @@ func (w *window) unmap() { | |||||
| 178 | return | 178 | return | ||
| 179 | } | 179 | } | ||
| 180 | w.mapped = false | 180 | w.mapped = false | ||
| 181 | + | w.xdg.surface.resetInputEmission() | |||
| 181 | w.xdg.configured = false | 182 | w.xdg.configured = false | ||
| 182 | w.xdg.acked = false | 183 | w.xdg.acked = false | ||
| 183 | w.xdg.serials = nil | 184 | w.xdg.serials = nil | ||
M internal/core/core.go +33 -5
| @@ -77,8 +77,9 @@ type Core struct { | |||
| 77 | 77 | terms map[string]*termSpawn | |
| 78 | 78 | firstTerminalResolved bool | |
| 79 | 79 | // clients holds the app ID and PID of mapped windows, for State. | |
| 80 | - | clients map[WindowID]ports.WindowMapped | |
| 81 | - | sentState ports.State | |
| 80 | + | clients map[WindowID]ports.WindowMapped | |
| 81 | + | inputRegions map[WindowID]ports.InputRegionChanged | |
| 82 | + | sentState ports.State | |
| 82 | 83 | // popups are the placed xdg_popups; popupOrder stacks them, oldest first. | |
| 83 | 84 | popups map[WindowID]*popupState | |
| 84 | 85 | popupOrder []WindowID | |
| @@ -288,7 +289,7 @@ func New(cfg ports.Config, ch Channels) (*Core, error) { | |||
| 288 | 289 | return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1") | |
| 289 | 290 | } | |
| 290 | 291 | // A placeholder screen holds windows until the first output arrives. | |
| 291 | - | c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}, inhibitors: map[WindowID]bool{}, idle: map[WindowID]bool{}} | |
| 292 | + | c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, inputRegions: map[WindowID]ports.InputRegionChanged{}, popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}, inhibitors: map[WindowID]bool{}, idle: map[WindowID]bool{}} | |
| 292 | 293 | if err := c.apply(cfg); err != nil { | |
| 293 | 294 | return nil, err | |
| 294 | 295 | } | |
| @@ -361,16 +362,24 @@ func (c *Core) keyboardFocus() WindowID { | |||
| 361 | 362 | // setLayers splits the mapped layer surfaces by output. A surface without an | |
| 362 | 363 | // output, or on an unknown one, goes to the focused output. | |
| 363 | 364 | func (c *Core) setLayers(all []ports.LayerSurface) { | |
| 365 | + | previous := make(map[WindowID]bool) | |
| 364 | 366 | for _, sc := range c.screens { | |
| 367 | + | for _, l := range sc.layers { | |
| 368 | + | previous[l.ID] = true | |
| 369 | + | } | |
| 365 | 370 | sc.layers = nil | |
| 366 | 371 | } | |
| 367 | 372 | for _, l := range all { | |
| 373 | + | delete(previous, l.ID) | |
| 368 | 374 | sc := c.cur() | |
| 369 | 375 | if i := c.screenIndex(l.Output); i >= 0 { | |
| 370 | 376 | sc = c.screens[i] | |
| 371 | 377 | } | |
| 372 | 378 | sc.layers = append(sc.layers, l) | |
| 373 | 379 | } | |
| 380 | + | for id := range previous { | |
| 381 | + | delete(c.inputRegions, id) | |
| 382 | + | } | |
| 374 | 383 | for _, sc := range c.screens { | |
| 375 | 384 | sc.arrange() | |
| 376 | 385 | } | |
| @@ -565,6 +574,19 @@ func (c *Core) allLayers() []ports.LayerSurface { | |||
| 565 | 574 | return all | |
| 566 | 575 | } | |
| 567 | 576 | ||
| 577 | + | func (c *Core) acceptsInput(id WindowID, x, y float64) bool { | |
| 578 | + | r, ok := c.inputRegions[id] | |
| 579 | + | if !ok || r.All { | |
| 580 | + | return true | |
| 581 | + | } | |
| 582 | + | for _, box := range r.Rects { | |
| 583 | + | if x >= float64(box.X) && x < float64(box.X+box.W) && y >= float64(box.Y) && y < float64(box.Y+box.H) { | |
| 584 | + | return true | |
| 585 | + | } | |
| 586 | + | } | |
| 587 | + | return false | |
| 588 | + | } | |
| 589 | + | ||
| 568 | 590 | // hit returns the surface under the global logical point and the point in | |
| 569 | 591 | // its surface coordinates: popups, then overlay and top layers, then | |
| 570 | 592 | // windows (fullscreen wins; otherwise the last visible placement is | |
| @@ -581,7 +603,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||
| 581 | 603 | if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 { | |
| 582 | 604 | return id, px, py | |
| 583 | 605 | } | |
| 584 | - | if id, px, py := layerAt(sc, lx, ly, true); id != 0 { | |
| 606 | + | if id, px, py := c.layerAt(sc, lx, ly, true); id != 0 { | |
| 585 | 607 | return id, px, py | |
| 586 | 608 | } | |
| 587 | 609 | if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 { | |
| @@ -598,6 +620,9 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||
| 598 | 620 | if full && !p.Floating { | |
| 599 | 621 | continue | |
| 600 | 622 | } | |
| 623 | + | if !c.acceptsInput(p.ID, lx-float64(r.X), ly-float64(r.Y)) { | |
| 624 | + | continue | |
| 625 | + | } | |
| 601 | 626 | if id == 0 || p.Fullscreen || p.Floating { | |
| 602 | 627 | id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y) | |
| 603 | 628 | } | |
| @@ -605,7 +630,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||
| 605 | 630 | } | |
| 606 | 631 | } | |
| 607 | 632 | if id == 0 { | |
| 608 | - | return layerAt(sc, lx, ly, false) | |
| 633 | + | return c.layerAt(sc, lx, ly, false) | |
| 609 | 634 | } | |
| 610 | 635 | return id, sx, sy | |
| 611 | 636 | } | |
| @@ -637,6 +662,8 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 637 | 662 | case ports.LayerChanged: | |
| 638 | 663 | c.layerChanged = true | |
| 639 | 664 | c.setLayers(v.Layers) | |
| 665 | + | case ports.InputRegionChanged: | |
| 666 | + | c.inputRegions[v.ID] = v | |
| 640 | 667 | case ports.WindowMapped: | |
| 641 | 668 | c.clients[v.ID] = v | |
| 642 | 669 | if v.Floating { | |
| @@ -697,6 +724,7 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 697 | 724 | return nil | |
| 698 | 725 | } | |
| 699 | 726 | delete(c.clients, v.ID) | |
| 727 | + | delete(c.inputRegions, v.ID) | |
| 700 | 728 | delete(c.inhibitors, v.ID) | |
| 701 | 729 | delete(c.idle, v.ID) | |
| 702 | 730 | if s, _ := c.screenOf(v.ID); s != nil { | |
| @@ -77,8 +77,9 @@ type Core struct { | |||||
| 77 | terms map[string]*termSpawn | 77 | terms map[string]*termSpawn | ||
| 78 | firstTerminalResolved bool | 78 | firstTerminalResolved bool | ||
| 79 | // clients holds the app ID and PID of mapped windows, for State. | 79 | // clients holds the app ID and PID of mapped windows, for State. | ||
| 80 | - | clients map[WindowID]ports.WindowMapped | 80 | + | clients map[WindowID]ports.WindowMapped |
| 81 | - | sentState ports.State | 81 | + | inputRegions map[WindowID]ports.InputRegionChanged |
| 82 | + | sentState ports.State | |||
| 82 | // popups are the placed xdg_popups; popupOrder stacks them, oldest first. | 83 | // popups are the placed xdg_popups; popupOrder stacks them, oldest first. | ||
| 83 | popups map[WindowID]*popupState | 84 | popups map[WindowID]*popupState | ||
| 84 | popupOrder []WindowID | 85 | popupOrder []WindowID | ||
| @@ -288,7 +289,7 @@ func New(cfg ports.Config, ch Channels) (*Core, error) { | |||||
| 288 | return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1") | 289 | return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1") | ||
| 289 | } | 290 | } | ||
| 290 | // A placeholder screen holds windows until the first output arrives. | 291 | // A placeholder screen holds windows until the first output arrives. | ||
| 291 | - | c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}, inhibitors: map[WindowID]bool{}, idle: map[WindowID]bool{}} | 292 | + | c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, inputRegions: map[WindowID]ports.InputRegionChanged{}, popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}, inhibitors: map[WindowID]bool{}, idle: map[WindowID]bool{}} |
| 292 | if err := c.apply(cfg); err != nil { | 293 | if err := c.apply(cfg); err != nil { | ||
| 293 | return nil, err | 294 | return nil, err | ||
| 294 | } | 295 | } | ||
| @@ -361,16 +362,24 @@ func (c *Core) keyboardFocus() WindowID { | |||||
| 361 | // setLayers splits the mapped layer surfaces by output. A surface without an | 362 | // setLayers splits the mapped layer surfaces by output. A surface without an | ||
| 362 | // output, or on an unknown one, goes to the focused output. | 363 | // output, or on an unknown one, goes to the focused output. | ||
| 363 | func (c *Core) setLayers(all []ports.LayerSurface) { | 364 | func (c *Core) setLayers(all []ports.LayerSurface) { | ||
| 365 | + | previous := make(map[WindowID]bool) | |||
| 364 | for _, sc := range c.screens { | 366 | for _, sc := range c.screens { | ||
| 367 | + | for _, l := range sc.layers { | |||
| 368 | + | previous[l.ID] = true | |||
| 369 | + | } | |||
| 365 | sc.layers = nil | 370 | sc.layers = nil | ||
| 366 | } | 371 | } | ||
| 367 | for _, l := range all { | 372 | for _, l := range all { | ||
| 373 | + | delete(previous, l.ID) | |||
| 368 | sc := c.cur() | 374 | sc := c.cur() | ||
| 369 | if i := c.screenIndex(l.Output); i >= 0 { | 375 | if i := c.screenIndex(l.Output); i >= 0 { | ||
| 370 | sc = c.screens[i] | 376 | sc = c.screens[i] | ||
| 371 | } | 377 | } | ||
| 372 | sc.layers = append(sc.layers, l) | 378 | sc.layers = append(sc.layers, l) | ||
| 373 | } | 379 | } | ||
| 380 | + | for id := range previous { | |||
| 381 | + | delete(c.inputRegions, id) | |||
| 382 | + | } | |||
| 374 | for _, sc := range c.screens { | 383 | for _, sc := range c.screens { | ||
| 375 | sc.arrange() | 384 | sc.arrange() | ||
| 376 | } | 385 | } | ||
| @@ -565,6 +574,19 @@ func (c *Core) allLayers() []ports.LayerSurface { | |||||
| 565 | return all | 574 | return all | ||
| 566 | } | 575 | } | ||
| 567 | 576 | ||||
| 577 | + | func (c *Core) acceptsInput(id WindowID, x, y float64) bool { | |||
| 578 | + | r, ok := c.inputRegions[id] | |||
| 579 | + | if !ok || r.All { | |||
| 580 | + | return true | |||
| 581 | + | } | |||
| 582 | + | for _, box := range r.Rects { | |||
| 583 | + | if x >= float64(box.X) && x < float64(box.X+box.W) && y >= float64(box.Y) && y < float64(box.Y+box.H) { | |||
| 584 | + | return true | |||
| 585 | + | } | |||
| 586 | + | } | |||
| 587 | + | return false | |||
| 588 | + | } | |||
| 589 | + | ||||
| 568 | // hit returns the surface under the global logical point and the point in | 590 | // hit returns the surface under the global logical point and the point in | ||
| 569 | // its surface coordinates: popups, then overlay and top layers, then | 591 | // its surface coordinates: popups, then overlay and top layers, then | ||
| 570 | // windows (fullscreen wins; otherwise the last visible placement is | 592 | // windows (fullscreen wins; otherwise the last visible placement is | ||
| @@ -581,7 +603,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||||
| 581 | if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 { | 603 | if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 { | ||
| 582 | return id, px, py | 604 | return id, px, py | ||
| 583 | } | 605 | } | ||
| 584 | - | if id, px, py := layerAt(sc, lx, ly, true); id != 0 { | 606 | + | if id, px, py := c.layerAt(sc, lx, ly, true); id != 0 { |
| 585 | return id, px, py | 607 | return id, px, py | ||
| 586 | } | 608 | } | ||
| 587 | if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 { | 609 | if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 { | ||
| @@ -598,6 +620,9 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||||
| 598 | if full && !p.Floating { | 620 | if full && !p.Floating { | ||
| 599 | continue | 621 | continue | ||
| 600 | } | 622 | } | ||
| 623 | + | if !c.acceptsInput(p.ID, lx-float64(r.X), ly-float64(r.Y)) { | |||
| 624 | + | continue | |||
| 625 | + | } | |||
| 601 | if id == 0 || p.Fullscreen || p.Floating { | 626 | if id == 0 || p.Fullscreen || p.Floating { | ||
| 602 | id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y) | 627 | id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y) | ||
| 603 | } | 628 | } | ||
| @@ -605,7 +630,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||||
| 605 | } | 630 | } | ||
| 606 | } | 631 | } | ||
| 607 | if id == 0 { | 632 | if id == 0 { | ||
| 608 | - | return layerAt(sc, lx, ly, false) | 633 | + | return c.layerAt(sc, lx, ly, false) |
| 609 | } | 634 | } | ||
| 610 | return id, sx, sy | 635 | return id, sx, sy | ||
| 611 | } | 636 | } | ||
| @@ -637,6 +662,8 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 637 | case ports.LayerChanged: | 662 | case ports.LayerChanged: | ||
| 638 | c.layerChanged = true | 663 | c.layerChanged = true | ||
| 639 | c.setLayers(v.Layers) | 664 | c.setLayers(v.Layers) | ||
| 665 | + | case ports.InputRegionChanged: | |||
| 666 | + | c.inputRegions[v.ID] = v | |||
| 640 | case ports.WindowMapped: | 667 | case ports.WindowMapped: | ||
| 641 | c.clients[v.ID] = v | 668 | c.clients[v.ID] = v | ||
| 642 | if v.Floating { | 669 | if v.Floating { | ||
| @@ -697,6 +724,7 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 697 | return nil | 724 | return nil | ||
| 698 | } | 725 | } | ||
| 699 | delete(c.clients, v.ID) | 726 | delete(c.clients, v.ID) | ||
| 727 | + | delete(c.inputRegions, v.ID) | |||
| 700 | delete(c.inhibitors, v.ID) | 728 | delete(c.inhibitors, v.ID) | ||
| 701 | delete(c.idle, v.ID) | 729 | delete(c.idle, v.ID) | ||
| 702 | if s, _ := c.screenOf(v.ID); s != nil { | 730 | if s, _ := c.screenOf(v.ID); s != nil { | ||
M internal/core/layer_input_test.go +124 -0
| @@ -269,3 +269,127 @@ func TestLayerFocusAcrossOutputs(t *testing.T) { | |||
| 269 | 269 | t.Fatalf("key to %d, want the new window", v.ID) | |
| 270 | 270 | } | |
| 271 | 271 | } | |
| 272 | + | ||
| 273 | + | // A layer's transparent margin passes pointer focus through to the window. | |
| 274 | + | func TestLayerInputRegionPassThrough(t *testing.T) { | |
| 275 | + | cfg := config.Defaults() | |
| 276 | + | cfg.Border.Width = 0 | |
| 277 | + | client := make(chan ports.ClientEvent, 16) | |
| 278 | + | input := make(chan ports.InputEvent, 16) | |
| 279 | + | output := make(chan ports.OutputEvent, 16) | |
| 280 | + | commands := make(chan ports.ClientCommand, 256) | |
| 281 | + | scenes := make(chan []ports.Scene, 1) | |
| 282 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 283 | + | if err != nil { | |
| 284 | + | t.Fatal(err) | |
| 285 | + | } | |
| 286 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 287 | + | defer cancel() | |
| 288 | + | go c.Run(ctx) | |
| 289 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |
| 290 | + | scene(t, scenes) | |
| 291 | + | client <- ports.WindowMapped{ID: 1} | |
| 292 | + | scene(t, scenes) | |
| 293 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{{ID: 6, Layer: ports.LayerOverlay, Width: 100, Height: 80}}} | |
| 294 | + | scene(t, scenes) | |
| 295 | + | client <- ports.InputRegionChanged{ID: 6, Rects: []ports.Rect{{X: 10, Y: 10, W: 80, H: 60}}} | |
| 296 | + | scene(t, scenes) | |
| 297 | + | input <- ports.PointerMotion{X: 5, Y: 5} | |
| 298 | + | if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 1 { | |
| 299 | + | t.Fatalf("margin focus: %+v", v) | |
| 300 | + | } | |
| 301 | + | input <- ports.PointerMotion{X: 20, Y: 20} | |
| 302 | + | if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 6 { | |
| 303 | + | t.Fatalf("interior focus: %+v", v) | |
| 304 | + | } | |
| 305 | + | client <- ports.InputRegionChanged{ID: 6} | |
| 306 | + | scene(t, scenes) | |
| 307 | + | input <- ports.PointerMotion{X: 20, Y: 21} | |
| 308 | + | if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 1 { | |
| 309 | + | t.Fatalf("empty focus: %+v", v) | |
| 310 | + | } | |
| 311 | + | client <- ports.InputRegionChanged{ID: 6, All: true} | |
| 312 | + | scene(t, scenes) | |
| 313 | + | input <- ports.PointerMotion{X: 5, Y: 5} | |
| 314 | + | if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 6 { | |
| 315 | + | t.Fatalf("reset focus: %+v", v) | |
| 316 | + | } | |
| 317 | + | } | |
| 318 | + | ||
| 319 | + | func TestRemovedLayerInputRegionCleared(t *testing.T) { | |
| 320 | + | cfg := config.Defaults() | |
| 321 | + | cfg.Border.Width = 0 | |
| 322 | + | client := make(chan ports.ClientEvent, 16) | |
| 323 | + | input := make(chan ports.InputEvent, 16) | |
| 324 | + | output := make(chan ports.OutputEvent, 16) | |
| 325 | + | commands := make(chan ports.ClientCommand, 256) | |
| 326 | + | scenes := make(chan []ports.Scene, 1) | |
| 327 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 328 | + | if err != nil { | |
| 329 | + | t.Fatal(err) | |
| 330 | + | } | |
| 331 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 332 | + | defer cancel() | |
| 333 | + | go c.Run(ctx) | |
| 334 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |
| 335 | + | scene(t, scenes) | |
| 336 | + | client <- ports.WindowMapped{ID: 1} | |
| 337 | + | scene(t, scenes) | |
| 338 | + | client <- ports.InputRegionChanged{ID: 1} | |
| 339 | + | scene(t, scenes) // window's empty region must survive layer changes | |
| 340 | + | layer := ports.LayerSurface{ID: 6, Layer: ports.LayerOverlay, Width: 100, Height: 80} | |
| 341 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{layer}} | |
| 342 | + | scene(t, scenes) | |
| 343 | + | client <- ports.InputRegionChanged{ID: 6} | |
| 344 | + | scene(t, scenes) | |
| 345 | + | client <- ports.LayerChanged{} | |
| 346 | + | scene(t, scenes) | |
| 347 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{layer}} | |
| 348 | + | scene(t, scenes) | |
| 349 | + | input <- ports.PointerMotion{X: 20, Y: 20} | |
| 350 | + | if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 6 { | |
| 351 | + | t.Fatalf("remapped layer retains old region: %+v", v) | |
| 352 | + | } | |
| 353 | + | client <- ports.LayerChanged{} | |
| 354 | + | scene(t, scenes) | |
| 355 | + | input <- ports.PointerMotion{X: 21, Y: 21} | |
| 356 | + | if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 0 { | |
| 357 | + | t.Fatalf("window region removed with layer: %+v", v) | |
| 358 | + | } | |
| 359 | + | } | |
| 360 | + | ||
| 361 | + | func TestPopupInputRegionPassesThroughToWindow(t *testing.T) { | |
| 362 | + | cfg := config.Defaults() | |
| 363 | + | cfg.Border.Width = 0 | |
| 364 | + | client := make(chan ports.ClientEvent, 16) | |
| 365 | + | input := make(chan ports.InputEvent, 16) | |
| 366 | + | output := make(chan ports.OutputEvent, 16) | |
| 367 | + | commands := make(chan ports.ClientCommand, 256) | |
| 368 | + | scenes := make(chan []ports.Scene, 1) | |
| 369 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 370 | + | if err != nil { | |
| 371 | + | t.Fatal(err) | |
| 372 | + | } | |
| 373 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 374 | + | defer cancel() | |
| 375 | + | go c.Run(ctx) | |
| 376 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |
| 377 | + | scene(t, scenes) | |
| 378 | + | client <- ports.WindowMapped{ID: 1} | |
| 379 | + | scene(t, scenes) | |
| 380 | + | client <- ports.PopupRequest{ID: 9, Parent: 1, Positioner: ports.Positioner{Width: 30, Height: 20, AnchorRect: ports.Rect{X: 10, Y: 10, W: 10, H: 10}}} | |
| 381 | + | next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 9 }) | |
| 382 | + | client <- ports.PopupMapped{ID: 9} | |
| 383 | + | sc := sceneMatch(t, scenes, func(sc ports.Scene) bool { return len(sc.Windows) == 2 }) | |
| 384 | + | client <- ports.InputRegionChanged{ID: 9, Rects: []ports.Rect{{X: 10, Y: 0, W: 20, H: 20}}} | |
| 385 | + | scene(t, scenes) | |
| 386 | + | popup := sc.Windows[1].Rect | |
| 387 | + | input <- ports.PointerMotion{X: float64(popup.X + 2), Y: float64(popup.Y + 2)} | |
| 388 | + | if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 1 { | |
| 389 | + | t.Fatalf("popup margin motion: %+v, popup %+v", v, popup) | |
| 390 | + | } | |
| 391 | + | input <- ports.PointerMotion{X: float64(popup.X + 12), Y: float64(popup.Y + 2)} | |
| 392 | + | if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 9 { | |
| 393 | + | t.Fatalf("popup interior motion: %+v", v) | |
| 394 | + | } | |
| 395 | + | } | |
| @@ -269,3 +269,127 @@ func TestLayerFocusAcrossOutputs(t *testing.T) { | |||||
| 269 | t.Fatalf("key to %d, want the new window", v.ID) | 269 | t.Fatalf("key to %d, want the new window", v.ID) | ||
| 270 | } | 270 | } | ||
| 271 | } | 271 | } | ||
| 272 | + | ||||
| 273 | + | // A layer's transparent margin passes pointer focus through to the window. | |||
| 274 | + | func TestLayerInputRegionPassThrough(t *testing.T) { | |||
| 275 | + | cfg := config.Defaults() | |||
| 276 | + | cfg.Border.Width = 0 | |||
| 277 | + | client := make(chan ports.ClientEvent, 16) | |||
| 278 | + | input := make(chan ports.InputEvent, 16) | |||
| 279 | + | output := make(chan ports.OutputEvent, 16) | |||
| 280 | + | commands := make(chan ports.ClientCommand, 256) | |||
| 281 | + | scenes := make(chan []ports.Scene, 1) | |||
| 282 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |||
| 283 | + | if err != nil { | |||
| 284 | + | t.Fatal(err) | |||
| 285 | + | } | |||
| 286 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 287 | + | defer cancel() | |||
| 288 | + | go c.Run(ctx) | |||
| 289 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |||
| 290 | + | scene(t, scenes) | |||
| 291 | + | client <- ports.WindowMapped{ID: 1} | |||
| 292 | + | scene(t, scenes) | |||
| 293 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{{ID: 6, Layer: ports.LayerOverlay, Width: 100, Height: 80}}} | |||
| 294 | + | scene(t, scenes) | |||
| 295 | + | client <- ports.InputRegionChanged{ID: 6, Rects: []ports.Rect{{X: 10, Y: 10, W: 80, H: 60}}} | |||
| 296 | + | scene(t, scenes) | |||
| 297 | + | input <- ports.PointerMotion{X: 5, Y: 5} | |||
| 298 | + | if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 1 { | |||
| 299 | + | t.Fatalf("margin focus: %+v", v) | |||
| 300 | + | } | |||
| 301 | + | input <- ports.PointerMotion{X: 20, Y: 20} | |||
| 302 | + | if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 6 { | |||
| 303 | + | t.Fatalf("interior focus: %+v", v) | |||
| 304 | + | } | |||
| 305 | + | client <- ports.InputRegionChanged{ID: 6} | |||
| 306 | + | scene(t, scenes) | |||
| 307 | + | input <- ports.PointerMotion{X: 20, Y: 21} | |||
| 308 | + | if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 1 { | |||
| 309 | + | t.Fatalf("empty focus: %+v", v) | |||
| 310 | + | } | |||
| 311 | + | client <- ports.InputRegionChanged{ID: 6, All: true} | |||
| 312 | + | scene(t, scenes) | |||
| 313 | + | input <- ports.PointerMotion{X: 5, Y: 5} | |||
| 314 | + | if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 6 { | |||
| 315 | + | t.Fatalf("reset focus: %+v", v) | |||
| 316 | + | } | |||
| 317 | + | } | |||
| 318 | + | ||||
| 319 | + | func TestRemovedLayerInputRegionCleared(t *testing.T) { | |||
| 320 | + | cfg := config.Defaults() | |||
| 321 | + | cfg.Border.Width = 0 | |||
| 322 | + | client := make(chan ports.ClientEvent, 16) | |||
| 323 | + | input := make(chan ports.InputEvent, 16) | |||
| 324 | + | output := make(chan ports.OutputEvent, 16) | |||
| 325 | + | commands := make(chan ports.ClientCommand, 256) | |||
| 326 | + | scenes := make(chan []ports.Scene, 1) | |||
| 327 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |||
| 328 | + | if err != nil { | |||
| 329 | + | t.Fatal(err) | |||
| 330 | + | } | |||
| 331 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 332 | + | defer cancel() | |||
| 333 | + | go c.Run(ctx) | |||
| 334 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |||
| 335 | + | scene(t, scenes) | |||
| 336 | + | client <- ports.WindowMapped{ID: 1} | |||
| 337 | + | scene(t, scenes) | |||
| 338 | + | client <- ports.InputRegionChanged{ID: 1} | |||
| 339 | + | scene(t, scenes) // window's empty region must survive layer changes | |||
| 340 | + | layer := ports.LayerSurface{ID: 6, Layer: ports.LayerOverlay, Width: 100, Height: 80} | |||
| 341 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{layer}} | |||
| 342 | + | scene(t, scenes) | |||
| 343 | + | client <- ports.InputRegionChanged{ID: 6} | |||
| 344 | + | scene(t, scenes) | |||
| 345 | + | client <- ports.LayerChanged{} | |||
| 346 | + | scene(t, scenes) | |||
| 347 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{layer}} | |||
| 348 | + | scene(t, scenes) | |||
| 349 | + | input <- ports.PointerMotion{X: 20, Y: 20} | |||
| 350 | + | if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 6 { | |||
| 351 | + | t.Fatalf("remapped layer retains old region: %+v", v) | |||
| 352 | + | } | |||
| 353 | + | client <- ports.LayerChanged{} | |||
| 354 | + | scene(t, scenes) | |||
| 355 | + | input <- ports.PointerMotion{X: 21, Y: 21} | |||
| 356 | + | if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 0 { | |||
| 357 | + | t.Fatalf("window region removed with layer: %+v", v) | |||
| 358 | + | } | |||
| 359 | + | } | |||
| 360 | + | ||||
| 361 | + | func TestPopupInputRegionPassesThroughToWindow(t *testing.T) { | |||
| 362 | + | cfg := config.Defaults() | |||
| 363 | + | cfg.Border.Width = 0 | |||
| 364 | + | client := make(chan ports.ClientEvent, 16) | |||
| 365 | + | input := make(chan ports.InputEvent, 16) | |||
| 366 | + | output := make(chan ports.OutputEvent, 16) | |||
| 367 | + | commands := make(chan ports.ClientCommand, 256) | |||
| 368 | + | scenes := make(chan []ports.Scene, 1) | |||
| 369 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |||
| 370 | + | if err != nil { | |||
| 371 | + | t.Fatal(err) | |||
| 372 | + | } | |||
| 373 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 374 | + | defer cancel() | |||
| 375 | + | go c.Run(ctx) | |||
| 376 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |||
| 377 | + | scene(t, scenes) | |||
| 378 | + | client <- ports.WindowMapped{ID: 1} | |||
| 379 | + | scene(t, scenes) | |||
| 380 | + | client <- ports.PopupRequest{ID: 9, Parent: 1, Positioner: ports.Positioner{Width: 30, Height: 20, AnchorRect: ports.Rect{X: 10, Y: 10, W: 10, H: 10}}} | |||
| 381 | + | next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 9 }) | |||
| 382 | + | client <- ports.PopupMapped{ID: 9} | |||
| 383 | + | sc := sceneMatch(t, scenes, func(sc ports.Scene) bool { return len(sc.Windows) == 2 }) | |||
| 384 | + | client <- ports.InputRegionChanged{ID: 9, Rects: []ports.Rect{{X: 10, Y: 0, W: 20, H: 20}}} | |||
| 385 | + | scene(t, scenes) | |||
| 386 | + | popup := sc.Windows[1].Rect | |||
| 387 | + | input <- ports.PointerMotion{X: float64(popup.X + 2), Y: float64(popup.Y + 2)} | |||
| 388 | + | if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 1 { | |||
| 389 | + | t.Fatalf("popup margin motion: %+v, popup %+v", v, popup) | |||
| 390 | + | } | |||
| 391 | + | input <- ports.PointerMotion{X: float64(popup.X + 12), Y: float64(popup.Y + 2)} | |||
| 392 | + | if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 9 { | |||
| 393 | + | t.Fatalf("popup interior motion: %+v", v) | |||
| 394 | + | } | |||
| 395 | + | } | |||
M internal/core/layers.go +2 -2
| @@ -104,7 +104,7 @@ func shown(sc *screen, layer ports.Layer) bool { | |||
| 104 | 104 | ||
| 105 | 105 | // layerAt returns the topmost shown layer surface under the output-local | |
| 106 | 106 | // point among those above (or below) the windows. | |
| 107 | - | func layerAt(sc *screen, lx, ly float64, above bool) (WindowID, float64, float64) { | |
| 107 | + | func (c *Core) layerAt(sc *screen, lx, ly float64, above bool) (WindowID, float64, float64) { | |
| 108 | 108 | full := hasFullscreen(sc) | |
| 109 | 109 | for i := len(sc.placed) - 1; i >= 0; i-- { | |
| 110 | 110 | l := sc.placed[i] | |
| @@ -112,7 +112,7 @@ func layerAt(sc *screen, lx, ly float64, above bool) (WindowID, float64, float64 | |||
| 112 | 112 | if (l.Layer >= ports.LayerTop) != above || (full && (l.Layer == ports.LayerBottom || l.Layer == ports.LayerTop)) || r.W <= 0 || r.H <= 0 { | |
| 113 | 113 | continue | |
| 114 | 114 | } | |
| 115 | - | if lx >= float64(r.X) && lx < float64(r.X+r.W) && ly >= float64(r.Y) && ly < float64(r.Y+r.H) { | |
| 115 | + | if lx >= float64(r.X) && lx < float64(r.X+r.W) && ly >= float64(r.Y) && ly < float64(r.Y+r.H) && c.acceptsInput(l.ID, lx-float64(r.X), ly-float64(r.Y)) { | |
| 116 | 116 | return l.ID, lx - float64(r.X), ly - float64(r.Y) | |
| 117 | 117 | } | |
| 118 | 118 | } | |
| @@ -104,7 +104,7 @@ func shown(sc *screen, layer ports.Layer) bool { | |||||
| 104 | 104 | ||||
| 105 | // layerAt returns the topmost shown layer surface under the output-local | 105 | // layerAt returns the topmost shown layer surface under the output-local | ||
| 106 | // point among those above (or below) the windows. | 106 | // point among those above (or below) the windows. | ||
| 107 | - | func layerAt(sc *screen, lx, ly float64, above bool) (WindowID, float64, float64) { | 107 | + | func (c *Core) layerAt(sc *screen, lx, ly float64, above bool) (WindowID, float64, float64) { |
| 108 | full := hasFullscreen(sc) | 108 | full := hasFullscreen(sc) | ||
| 109 | for i := len(sc.placed) - 1; i >= 0; i-- { | 109 | for i := len(sc.placed) - 1; i >= 0; i-- { | ||
| 110 | l := sc.placed[i] | 110 | l := sc.placed[i] | ||
| @@ -112,7 +112,7 @@ func layerAt(sc *screen, lx, ly float64, above bool) (WindowID, float64, float64 | |||||
| 112 | if (l.Layer >= ports.LayerTop) != above || (full && (l.Layer == ports.LayerBottom || l.Layer == ports.LayerTop)) || r.W <= 0 || r.H <= 0 { | 112 | if (l.Layer >= ports.LayerTop) != above || (full && (l.Layer == ports.LayerBottom || l.Layer == ports.LayerTop)) || r.W <= 0 || r.H <= 0 { | ||
| 113 | continue | 113 | continue | ||
| 114 | } | 114 | } | ||
| 115 | - | if lx >= float64(r.X) && lx < float64(r.X+r.W) && ly >= float64(r.Y) && ly < float64(r.Y+r.H) { | 115 | + | if lx >= float64(r.X) && lx < float64(r.X+r.W) && ly >= float64(r.Y) && ly < float64(r.Y+r.H) && c.acceptsInput(l.ID, lx-float64(r.X), ly-float64(r.Y)) { |
| 116 | return l.ID, lx - float64(r.X), ly - float64(r.Y) | 116 | return l.ID, lx - float64(r.X), ly - float64(r.Y) | ||
| 117 | } | 117 | } | ||
| 118 | } | 118 | } | ||
M internal/core/popup.go +1 -1
| @@ -290,7 +290,7 @@ func (c *Core) popupAt(sc *screen, lx, ly float64, overLayers bool) (WindowID, f | |||
| 290 | 290 | if !ok || s != sc { | |
| 291 | 291 | continue | |
| 292 | 292 | } | |
| 293 | - | if lx >= float64(r.X) && lx < float64(r.X+r.W) && ly >= float64(r.Y) && ly < float64(r.Y+r.H) { | |
| 293 | + | if lx >= float64(r.X) && lx < float64(r.X+r.W) && ly >= float64(r.Y) && ly < float64(r.Y+r.H) && c.acceptsInput(p.id, lx-float64(r.X), ly-float64(r.Y)) { | |
| 294 | 294 | return p.id, lx - float64(r.X), ly - float64(r.Y) | |
| 295 | 295 | } | |
| 296 | 296 | } | |
| @@ -290,7 +290,7 @@ func (c *Core) popupAt(sc *screen, lx, ly float64, overLayers bool) (WindowID, f | |||||
| 290 | if !ok || s != sc { | 290 | if !ok || s != sc { | ||
| 291 | continue | 291 | continue | ||
| 292 | } | 292 | } | ||
| 293 | - | if lx >= float64(r.X) && lx < float64(r.X+r.W) && ly >= float64(r.Y) && ly < float64(r.Y+r.H) { | 293 | + | if lx >= float64(r.X) && lx < float64(r.X+r.W) && ly >= float64(r.Y) && ly < float64(r.Y+r.H) && c.acceptsInput(p.id, lx-float64(r.X), ly-float64(r.Y)) { |
| 294 | return p.id, lx - float64(r.X), ly - float64(r.Y) | 294 | return p.id, lx - float64(r.X), ly - float64(r.Y) | ||
| 295 | } | 295 | } | ||
| 296 | } | 296 | } | ||
M internal/ports/messages.go +10 -0
| @@ -50,6 +50,16 @@ const SlotEnv = "NEFERWL_SLOT" | |||
| 50 | 50 | ||
| 51 | 51 | func (WindowMapped) clientEvent() {} | |
| 52 | 52 | ||
| 53 | + | // InputRegionChanged replaces the surface-local hit area for a mapped surface. | |
| 54 | + | // All means the full hit rectangle; otherwise Rects is the exact union. | |
| 55 | + | type InputRegionChanged struct { | |
| 56 | + | ID WindowID | |
| 57 | + | All bool | |
| 58 | + | Rects []Rect | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | func (InputRegionChanged) clientEvent() {} | |
| 62 | + | ||
| 53 | 63 | // WindowUnmapped carries wayland → core removal. | |
| 54 | 64 | type WindowUnmapped struct{ ID WindowID } | |
| 55 | 65 | ||
| @@ -50,6 +50,16 @@ const SlotEnv = "NEFERWL_SLOT" | |||||
| 50 | 50 | ||||
| 51 | func (WindowMapped) clientEvent() {} | 51 | func (WindowMapped) clientEvent() {} | ||
| 52 | 52 | ||||
| 53 | + | // InputRegionChanged replaces the surface-local hit area for a mapped surface. | |||
| 54 | + | // All means the full hit rectangle; otherwise Rects is the exact union. | |||
| 55 | + | type InputRegionChanged struct { | |||
| 56 | + | ID WindowID | |||
| 57 | + | All bool | |||
| 58 | + | Rects []Rect | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | func (InputRegionChanged) clientEvent() {} | |||
| 62 | + | ||||
| 53 | // WindowUnmapped carries wayland → core removal. | 63 | // WindowUnmapped carries wayland → core removal. | ||
| 54 | type WindowUnmapped struct{ ID WindowID } | 64 | type WindowUnmapped struct{ ID WindowID } | ||
| 55 | 65 | ||||