fix(wayland): honour wl_surface.set_input_region in hit-testing
- commit
- 5278439f02db5f08b7d9d7d08649a16ea3bc1e38
- author
- brice · · 2026-09-27 13:25 UTC
- parent
- 5731344
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
11 files · +461 -49
M internal/adapters/wayland/constraints.go +37 -2
| @@ -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 | } | |
| @@ -258,6 +258,28 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 258 | 258 | r.X -= x.geometry.X | |
| 259 | 259 | r.Y -= x.geometry.Y | |
| 260 | 260 | } | |
| 261 | + | // The input region also bounds confinement. A box is sufficient for core; | |
| 262 | + | // contains uses the exact region to decide activation. | |
| 263 | + | if mode == ports.ConstraintConfine { | |
| 264 | + | if all, rects := c.surface.effectiveInput(); !all { | |
| 265 | + | var box ports.Rect | |
| 266 | + | for _, input := range rects { | |
| 267 | + | if r.W > 0 && r.H > 0 { | |
| 268 | + | input = intersectRect(input, r) | |
| 269 | + | } | |
| 270 | + | if input.W <= 0 { | |
| 271 | + | continue | |
| 272 | + | } | |
| 273 | + | if box.W == 0 { | |
| 274 | + | box = input | |
| 275 | + | } else { | |
| 276 | + | x0, y0 := min(box.X, input.X), min(box.Y, input.Y) | |
| 277 | + | 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} | |
| 278 | + | } | |
| 279 | + | } | |
| 280 | + | r = box | |
| 281 | + | } | |
| 282 | + | } | |
| 261 | 283 | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | |
| 262 | 284 | } | |
| 263 | 285 | ||
| @@ -265,10 +287,23 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 265 | 287 | // activates only there, so it never warps the pointer. | |
| 266 | 288 | func (c *constraint) contains(w *window) bool { | |
| 267 | 289 | r := c.region | |
| 290 | + | x, y := w.surfacePoint(w.xdg.server.pointerX, w.xdg.server.pointerY) | |
| 291 | + | all, rects := c.surface.effectiveInput() | |
| 292 | + | if !all { | |
| 293 | + | inside := false | |
| 294 | + | for _, input := range rects { | |
| 295 | + | if x-float64(w.xdg.geometry.X) >= float64(input.X) && x-float64(w.xdg.geometry.X) < float64(input.X+input.W) && y-float64(w.xdg.geometry.Y) >= float64(input.Y) && y-float64(w.xdg.geometry.Y) < float64(input.Y+input.H) { | |
| 296 | + | inside = true | |
| 297 | + | break | |
| 298 | + | } | |
| 299 | + | } | |
| 300 | + | if !inside { | |
| 301 | + | return false | |
| 302 | + | } | |
| 303 | + | } | |
| 268 | 304 | if r.W <= 0 || r.H <= 0 { | |
| 269 | 305 | return true | |
| 270 | 306 | } | |
| 271 | - | x, y := w.surfacePoint(w.xdg.server.pointerX, w.xdg.server.pointerY) | |
| 272 | 307 | return x >= float64(r.X) && x < float64(r.X+r.W) && y >= float64(r.Y) && y < float64(r.Y+r.H) | |
| 273 | 308 | } | |
| 274 | 309 | ||
| @@ -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 | } | ||
| @@ -258,6 +258,28 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 258 | r.X -= x.geometry.X | 258 | r.X -= x.geometry.X | ||
| 259 | r.Y -= x.geometry.Y | 259 | r.Y -= x.geometry.Y | ||
| 260 | } | 260 | } | ||
| 261 | + | // The input region also bounds confinement. A box is sufficient for core; | |||
| 262 | + | // contains uses the exact region to decide activation. | |||
| 263 | + | if mode == ports.ConstraintConfine { | |||
| 264 | + | if all, rects := c.surface.effectiveInput(); !all { | |||
| 265 | + | var box ports.Rect | |||
| 266 | + | for _, input := range rects { | |||
| 267 | + | if r.W > 0 && r.H > 0 { | |||
| 268 | + | input = intersectRect(input, r) | |||
| 269 | + | } | |||
| 270 | + | if input.W <= 0 { | |||
| 271 | + | continue | |||
| 272 | + | } | |||
| 273 | + | if box.W == 0 { | |||
| 274 | + | box = input | |||
| 275 | + | } else { | |||
| 276 | + | x0, y0 := min(box.X, input.X), min(box.Y, input.Y) | |||
| 277 | + | 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} | |||
| 278 | + | } | |||
| 279 | + | } | |||
| 280 | + | r = box | |||
| 281 | + | } | |||
| 282 | + | } | |||
| 261 | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | 283 | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | ||
| 262 | } | 284 | } | ||
| 263 | 285 | ||||
| @@ -265,10 +287,23 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 265 | // activates only there, so it never warps the pointer. | 287 | // activates only there, so it never warps the pointer. | ||
| 266 | func (c *constraint) contains(w *window) bool { | 288 | func (c *constraint) contains(w *window) bool { | ||
| 267 | r := c.region | 289 | r := c.region | ||
| 290 | + | x, y := w.surfacePoint(w.xdg.server.pointerX, w.xdg.server.pointerY) | |||
| 291 | + | all, rects := c.surface.effectiveInput() | |||
| 292 | + | if !all { | |||
| 293 | + | inside := false | |||
| 294 | + | for _, input := range rects { | |||
| 295 | + | if x-float64(w.xdg.geometry.X) >= float64(input.X) && x-float64(w.xdg.geometry.X) < float64(input.X+input.W) && y-float64(w.xdg.geometry.Y) >= float64(input.Y) && y-float64(w.xdg.geometry.Y) < float64(input.Y+input.H) { | |||
| 296 | + | inside = true | |||
| 297 | + | break | |||
| 298 | + | } | |||
| 299 | + | } | |||
| 300 | + | if !inside { | |||
| 301 | + | return false | |||
| 302 | + | } | |||
| 303 | + | } | |||
| 268 | if r.W <= 0 || r.H <= 0 { | 304 | if r.W <= 0 || r.H <= 0 { | ||
| 269 | return true | 305 | return true | ||
| 270 | } | 306 | } | ||
| 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) | 307 | return x >= float64(r.X) && x < float64(r.X+r.W) && y >= float64(r.Y) && y < float64(r.Y+r.H) | ||
| 273 | } | 308 | } | ||
| 274 | 309 | ||||
M internal/adapters/wayland/fifo.go +31 -25
| @@ -198,31 +198,33 @@ 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 | - | moves []childMove | |
| 211 | - | xdg *xdgSurface | |
| 212 | - | geometry ports.Rect | |
| 213 | - | cons *constraint | |
| 214 | - | region *ports.Rect | |
| 215 | - | layer *layerSurface | |
| 216 | - | layerNext layerState | |
| 217 | - | barrier bool | |
| 218 | - | wait bool | |
| 219 | - | at time.Time | |
| 220 | - | feedback []*presentationtime.WpPresentationFeedback | |
| 221 | - | sync *commitSync | |
| 222 | - | color SurfaceColor | |
| 223 | - | representation surfaceRepresentation | |
| 224 | - | damage []ports.Rect | |
| 225 | - | bufDamage []ports.Rect | |
| 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 | + | moves []childMove | |
| 213 | + | xdg *xdgSurface | |
| 214 | + | geometry ports.Rect | |
| 215 | + | cons *constraint | |
| 216 | + | region *ports.Rect | |
| 217 | + | layer *layerSurface | |
| 218 | + | layerNext layerState | |
| 219 | + | barrier bool | |
| 220 | + | wait bool | |
| 221 | + | at time.Time | |
| 222 | + | feedback []*presentationtime.WpPresentationFeedback | |
| 223 | + | sync *commitSync | |
| 224 | + | color SurfaceColor | |
| 225 | + | representation surfaceRepresentation | |
| 226 | + | damage []ports.Rect | |
| 227 | + | bufDamage []ports.Rect | |
| 226 | 228 | } | |
| 227 | 229 | ||
| 228 | 230 | type childMove struct { | |
| @@ -234,6 +236,9 @@ type childMove struct { | |||
| 234 | 236 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | |
| 235 | 237 | func (s *surface) takePending() update { | |
| 236 | 238 | 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} | |
| 239 | + | u.inputSet, u.inputAll, u.inputRects = s.pendingInputSet, s.pendingInputAll, s.pendingInputRects | |
| 240 | + | s.pendingInputSet = false | |
| 241 | + | s.pendingInputRects = nil | |
| 237 | 242 | s.attached, s.pending, s.callbacks = false, nil, nil | |
| 238 | 243 | s.pendingFeedback, s.pendingSync = nil, nil | |
| 239 | 244 | s.pendingDamage, s.pendingBufDamage = nil, nil | |
| @@ -262,6 +267,7 @@ func (s *surface) takePending() update { | |||
| 262 | 267 | // putPending makes an update the surface's pending state again. Parts | |
| 263 | 268 | // whose object is gone (viewport, child, role, constraint) are dropped. | |
| 264 | 269 | func (s *surface) putPending(u update) { | |
| 270 | + | s.pendingInputSet, s.pendingInputAll, s.pendingInputRects = u.inputSet, u.inputAll, u.inputRects | |
| 265 | 271 | s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks | |
| 266 | 272 | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | |
| 267 | 273 | s.pendingColor = u.color | |
| @@ -198,31 +198,33 @@ 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 | - | moves []childMove | 210 | + | vpW, vpH int32 |
| 211 | - | xdg *xdgSurface | 211 | + | vpSet bool |
| 212 | - | geometry ports.Rect | 212 | + | moves []childMove |
| 213 | - | cons *constraint | 213 | + | xdg *xdgSurface |
| 214 | - | region *ports.Rect | 214 | + | geometry ports.Rect |
| 215 | - | layer *layerSurface | 215 | + | cons *constraint |
| 216 | - | layerNext layerState | 216 | + | region *ports.Rect |
| 217 | - | barrier bool | 217 | + | layer *layerSurface |
| 218 | - | wait bool | 218 | + | layerNext layerState |
| 219 | - | at time.Time | 219 | + | barrier bool |
| 220 | - | feedback []*presentationtime.WpPresentationFeedback | 220 | + | wait bool |
| 221 | - | sync *commitSync | 221 | + | at time.Time |
| 222 | - | color SurfaceColor | 222 | + | feedback []*presentationtime.WpPresentationFeedback |
| 223 | - | representation surfaceRepresentation | 223 | + | sync *commitSync |
| 224 | - | damage []ports.Rect | 224 | + | color SurfaceColor |
| 225 | - | bufDamage []ports.Rect | 225 | + | representation surfaceRepresentation |
| 226 | + | damage []ports.Rect | |||
| 227 | + | bufDamage []ports.Rect | |||
| 226 | } | 228 | } | ||
| 227 | 229 | ||||
| 228 | type childMove struct { | 230 | type childMove struct { | ||
| @@ -234,6 +236,9 @@ type childMove struct { | |||||
| 234 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | 236 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | ||
| 235 | func (s *surface) takePending() update { | 237 | func (s *surface) takePending() update { | ||
| 236 | 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} | 238 | 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} | ||
| 239 | + | u.inputSet, u.inputAll, u.inputRects = s.pendingInputSet, s.pendingInputAll, s.pendingInputRects | |||
| 240 | + | s.pendingInputSet = false | |||
| 241 | + | s.pendingInputRects = nil | |||
| 237 | s.attached, s.pending, s.callbacks = false, nil, nil | 242 | s.attached, s.pending, s.callbacks = false, nil, nil | ||
| 238 | s.pendingFeedback, s.pendingSync = nil, nil | 243 | s.pendingFeedback, s.pendingSync = nil, nil | ||
| 239 | s.pendingDamage, s.pendingBufDamage = nil, nil | 244 | s.pendingDamage, s.pendingBufDamage = nil, nil | ||
| @@ -262,6 +267,7 @@ func (s *surface) takePending() update { | |||||
| 262 | // putPending makes an update the surface's pending state again. Parts | 267 | // putPending makes an update the surface's pending state again. Parts | ||
| 263 | // whose object is gone (viewport, child, role, constraint) are dropped. | 268 | // whose object is gone (viewport, child, role, constraint) are dropped. | ||
| 264 | func (s *surface) putPending(u update) { | 269 | func (s *surface) putPending(u update) { | ||
| 270 | + | s.pendingInputSet, s.pendingInputAll, s.pendingInputRects = u.inputSet, u.inputAll, u.inputRects | |||
| 265 | s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks | 271 | s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks | ||
| 266 | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | 272 | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | ||
| 267 | s.pendingColor = u.color | 273 | 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 | state.wl = w | |
| 79 | 79 | c.server.surfaces[w.Resource] = state | |
| @@ -122,6 +122,7 @@ func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, par | |||
| 122 | 122 | up.sub.children = append(up.sub.children, state) | |
| 123 | 123 | // The child follows its root window's output and scale. | |
| 124 | 124 | state.sendScale() | |
| 125 | + | up.emitInput() | |
| 125 | 126 | sub.OnDestroy = func() { | |
| 126 | 127 | if state.sub.role != sub { | |
| 127 | 128 | return | |
| @@ -212,23 +213,40 @@ func sortBelowFirst(list []*surface) { | |||
| 212 | 213 | copy(list, append(below, above...)) | |
| 213 | 214 | } | |
| 214 | 215 | ||
| 215 | - | // region keeps the bounding box of the rectangles added: pointer | |
| 216 | - | // confinement is the only user, and subtracted areas are ignored. | |
| 217 | - | type region struct{ box ports.Rect } | |
| 216 | + | // region stores the exact union of added rectangles. | |
| 217 | + | type region struct{ rects []ports.Rect } | |
| 218 | 218 | ||
| 219 | 219 | func (*region) Destroy(*wayland.Region) {} | |
| 220 | 220 | func (g *region) Add(_ *wayland.Region, x, y, w, h int32) { | |
| 221 | + | if w > 0 && h > 0 { | |
| 222 | + | g.rects = append(g.rects, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)}) | |
| 223 | + | } | |
| 224 | + | } | |
| 225 | + | func (g *region) Subtract(_ *wayland.Region, x, y, w, h int32) { | |
| 221 | 226 | if w <= 0 || h <= 0 { | |
| 222 | 227 | return | |
| 223 | 228 | } | |
| 224 | - | r := ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)} | |
| 225 | - | if g.box.W > 0 { | |
| 226 | - | x0, y0 := min(g.box.X, r.X), min(g.box.Y, r.Y) | |
| 227 | - | 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} | |
| 229 | + | cut := ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)} | |
| 230 | + | var out []ports.Rect | |
| 231 | + | for _, r := range g.rects { | |
| 232 | + | i := intersectRect(r, cut) | |
| 233 | + | if i.W == 0 { | |
| 234 | + | out = append(out, r) | |
| 235 | + | continue | |
| 236 | + | } | |
| 237 | + | for _, piece := range []ports.Rect{ | |
| 238 | + | {X: r.X, Y: r.Y, W: r.W, H: i.Y - r.Y}, | |
| 239 | + | {X: r.X, Y: i.Y + i.H, W: r.W, H: r.Y + r.H - i.Y - i.H}, | |
| 240 | + | {X: r.X, Y: i.Y, W: i.X - r.X, H: i.H}, | |
| 241 | + | {X: i.X + i.W, Y: i.Y, W: r.X + r.W - i.X - i.W, H: i.H}, | |
| 242 | + | } { | |
| 243 | + | if piece.W > 0 && piece.H > 0 { | |
| 244 | + | out = append(out, piece) | |
| 245 | + | } | |
| 246 | + | } | |
| 228 | 247 | } | |
| 229 | - | g.box = r | |
| 248 | + | g.rects = out | |
| 230 | 249 | } | |
| 231 | - | func (*region) Subtract(*wayland.Region, int32, int32, int32, int32) {} | |
| 232 | 250 | ||
| 233 | 251 | type shm struct{ server *Server } | |
| 234 | 252 | ||
| @@ -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 | state.wl = w | 78 | state.wl = w | ||
| 79 | c.server.surfaces[w.Resource] = state | 79 | c.server.surfaces[w.Resource] = state | ||
| @@ -122,6 +122,7 @@ func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, par | |||||
| 122 | up.sub.children = append(up.sub.children, state) | 122 | up.sub.children = append(up.sub.children, state) | ||
| 123 | // The child follows its root window's output and scale. | 123 | // The child follows its root window's output and scale. | ||
| 124 | state.sendScale() | 124 | state.sendScale() | ||
| 125 | + | up.emitInput() | |||
| 125 | sub.OnDestroy = func() { | 126 | sub.OnDestroy = func() { | ||
| 126 | if state.sub.role != sub { | 127 | if state.sub.role != sub { | ||
| 127 | return | 128 | return | ||
| @@ -212,23 +213,40 @@ func sortBelowFirst(list []*surface) { | |||||
| 212 | copy(list, append(below, above...)) | 213 | copy(list, append(below, above...)) | ||
| 213 | } | 214 | } | ||
| 214 | 215 | ||||
| 215 | - | // region keeps the bounding box of the rectangles added: pointer | 216 | + | // region stores the exact union of added rectangles. |
| 216 | - | // confinement is the only user, and subtracted areas are ignored. | 217 | + | type region struct{ rects []ports.Rect } |
| 217 | - | type region struct{ box ports.Rect } | |||
| 218 | 218 | ||||
| 219 | func (*region) Destroy(*wayland.Region) {} | 219 | func (*region) Destroy(*wayland.Region) {} | ||
| 220 | func (g *region) Add(_ *wayland.Region, x, y, w, h int32) { | 220 | func (g *region) Add(_ *wayland.Region, x, y, w, h int32) { | ||
| 221 | + | if w > 0 && h > 0 { | |||
| 222 | + | g.rects = append(g.rects, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)}) | |||
| 223 | + | } | |||
| 224 | + | } | |||
| 225 | + | func (g *region) Subtract(_ *wayland.Region, x, y, w, h int32) { | |||
| 221 | if w <= 0 || h <= 0 { | 226 | if w <= 0 || h <= 0 { | ||
| 222 | return | 227 | return | ||
| 223 | } | 228 | } | ||
| 224 | - | r := ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)} | 229 | + | cut := ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)} |
| 225 | - | if g.box.W > 0 { | 230 | + | var out []ports.Rect |
| 226 | - | x0, y0 := min(g.box.X, r.X), min(g.box.Y, r.Y) | 231 | + | for _, r := range g.rects { |
| 227 | - | 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} | 232 | + | i := intersectRect(r, cut) |
| 233 | + | if i.W == 0 { | |||
| 234 | + | out = append(out, r) | |||
| 235 | + | continue | |||
| 236 | + | } | |||
| 237 | + | for _, piece := range []ports.Rect{ | |||
| 238 | + | {X: r.X, Y: r.Y, W: r.W, H: i.Y - r.Y}, | |||
| 239 | + | {X: r.X, Y: i.Y + i.H, W: r.W, H: r.Y + r.H - i.Y - i.H}, | |||
| 240 | + | {X: r.X, Y: i.Y, W: i.X - r.X, H: i.H}, | |||
| 241 | + | {X: i.X + i.W, Y: i.Y, W: r.X + r.W - i.X - i.W, H: i.H}, | |||
| 242 | + | } { | |||
| 243 | + | if piece.W > 0 && piece.H > 0 { | |||
| 244 | + | out = append(out, piece) | |||
| 245 | + | } | |||
| 246 | + | } | |||
| 228 | } | 247 | } | ||
| 229 | - | g.box = r | 248 | + | g.rects = out |
| 230 | } | 249 | } | ||
| 231 | - | func (*region) Subtract(*wayland.Region, int32, int32, int32, int32) {} | |||
| 232 | 250 | ||||
| 233 | type shm struct{ server *Server } | 251 | type shm struct{ server *Server } | ||
| 234 | 252 | ||||
A internal/adapters/wayland/input_region.go +108 -0
| @@ -0,0 +1,108 @@ | |||
| 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 _, ch := range v.sub.children { | |
| 57 | + | visit(ch, x+ch.sub.x, y+ch.sub.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 | + | func (s *surface) emitInput() { | |
| 74 | + | root := s.root() | |
| 75 | + | id := root.windowID() | |
| 76 | + | if id == 0 { | |
| 77 | + | return | |
| 78 | + | } | |
| 79 | + | all, rects := root.effectiveInput() | |
| 80 | + | if !root.sentInput && all { | |
| 81 | + | return | |
| 82 | + | } | |
| 83 | + | if root.sentInput && root.lastInputAll == all && slices.Equal(root.lastInputRects, rects) { | |
| 84 | + | return | |
| 85 | + | } | |
| 86 | + | root.sentInput = true | |
| 87 | + | root.lastInputAll = all | |
| 88 | + | root.lastInputRects = slices.Clone(rects) | |
| 89 | + | s.server.emit(ports.InputRegionChanged{ID: id, All: all, Rects: rects}) | |
| 90 | + | if c := s.server.constraints[root]; c != nil { | |
| 91 | + | if c.active { | |
| 92 | + | s.server.emitConstraint(c) | |
| 93 | + | } else { | |
| 94 | + | s.server.updateConstraint() | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | func (s *surface) SetInputRegion(_ *wayland.Surface, reg *wayland.Region) { | |
| 100 | + | s.pendingInputSet = true | |
| 101 | + | s.pendingInputAll = reg == nil || reg.Resource == nil | |
| 102 | + | s.pendingInputRects = nil | |
| 103 | + | if !s.pendingInputAll { | |
| 104 | + | if g := s.server.regions[reg.Resource]; g != nil { | |
| 105 | + | s.pendingInputRects = slices.Clone(g.rects) | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | } | |
| @@ -0,0 +1,108 @@ | |||||
| 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 _, ch := range v.sub.children { | |||
| 57 | + | visit(ch, x+ch.sub.x, y+ch.sub.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 | + | func (s *surface) emitInput() { | |||
| 74 | + | root := s.root() | |||
| 75 | + | id := root.windowID() | |||
| 76 | + | if id == 0 { | |||
| 77 | + | return | |||
| 78 | + | } | |||
| 79 | + | all, rects := root.effectiveInput() | |||
| 80 | + | if !root.sentInput && all { | |||
| 81 | + | return | |||
| 82 | + | } | |||
| 83 | + | if root.sentInput && root.lastInputAll == all && slices.Equal(root.lastInputRects, rects) { | |||
| 84 | + | return | |||
| 85 | + | } | |||
| 86 | + | root.sentInput = true | |||
| 87 | + | root.lastInputAll = all | |||
| 88 | + | root.lastInputRects = slices.Clone(rects) | |||
| 89 | + | s.server.emit(ports.InputRegionChanged{ID: id, All: all, Rects: rects}) | |||
| 90 | + | if c := s.server.constraints[root]; c != nil { | |||
| 91 | + | if c.active { | |||
| 92 | + | s.server.emitConstraint(c) | |||
| 93 | + | } else { | |||
| 94 | + | s.server.updateConstraint() | |||
| 95 | + | } | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | func (s *surface) SetInputRegion(_ *wayland.Surface, reg *wayland.Region) { | |||
| 100 | + | s.pendingInputSet = true | |||
| 101 | + | s.pendingInputAll = reg == nil || reg.Resource == nil | |||
| 102 | + | s.pendingInputRects = nil | |||
| 103 | + | if !s.pendingInputAll { | |||
| 104 | + | if g := s.server.regions[reg.Resource]; g != nil { | |||
| 105 | + | s.pendingInputRects = slices.Clone(g.rects) | |||
| 106 | + | } | |||
| 107 | + | } | |||
| 108 | + | } | |||
A internal/adapters/wayland/input_region_test.go +159 -0
| @@ -0,0 +1,159 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/purego-libwayland/protocol/pointerconstraints" | |
| 5 | + | "golang.org/x/sys/unix" | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |
| 10 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | func inputChanged(t *testing.T, events <-chan ports.ClientEvent) ports.InputRegionChanged { | |
| 14 | + | t.Helper() | |
| 15 | + | for { | |
| 16 | + | if v, ok := waitEvent(t, events, 2*time.Second).(ports.InputRegionChanged); ok { | |
| 17 | + | return v | |
| 18 | + | } | |
| 19 | + | } | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | func TestInputRegionCommitCopySubtractAndReset(t *testing.T) { | |
| 23 | + | s, events, _, dir := lifecycleServer(t) | |
| 24 | + | c := protocolClient(t, s, dir) | |
| 25 | + | w, surf, _ := surfaceMapper(t, c, events)() | |
| 26 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 27 | + | reg := c.AllocateID() | |
| 28 | + | registerProtocol(t, c, reg) | |
| 29 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, reg) | |
| 30 | + | requestProtocol(t, c, reg, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1)) | |
| 31 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, reg) | |
| 32 | + | requestProtocol(t, c, reg, wayland.RegionRequestSubtract, int32(0), int32(0), int32(1), int32(1)) | |
| 33 | + | requestProtocol(t, c, reg, wayland.RegionRequestDestroy) | |
| 34 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 35 | + | if got := inputChanged(t, events); got.ID != w.ID || len(got.Rects) != 1 || got.Rects[0] != (ports.Rect{W: 1, H: 1}) { | |
| 36 | + | t.Fatalf("copied region: %+v", got) | |
| 37 | + | } | |
| 38 | + | empty := c.AllocateID() | |
| 39 | + | registerProtocol(t, c, empty) | |
| 40 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, empty) | |
| 41 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, empty) | |
| 42 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 43 | + | if got := inputChanged(t, events); got.All || len(got.Rects) != 0 { | |
| 44 | + | t.Fatalf("empty region: %+v", got) | |
| 45 | + | } | |
| 46 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, uint32(0)) | |
| 47 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 48 | + | if got := inputChanged(t, events); !got.All { | |
| 49 | + | t.Fatalf("reset region: %+v", got) | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | func TestRegionSubtractSplits(t *testing.T) { | |
| 54 | + | g := ®ion{} | |
| 55 | + | g.Add(nil, 0, 0, 10, 10) | |
| 56 | + | g.Subtract(nil, 2, 2, 6, 6) | |
| 57 | + | if len(g.rects) != 4 || g.boxRect() != (ports.Rect{W: 10, H: 10}) { | |
| 58 | + | t.Fatalf("subtract: %+v", g.rects) | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | func TestSubsurfaceInputRegionOwnCommitAndOffset(t *testing.T) { | |
| 63 | + | s, events, _, dir := lifecycleServer(t) | |
| 64 | + | c := protocolClient(t, s, dir) | |
| 65 | + | w, root, _ := surfaceMapper(t, c, events)() | |
| 66 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 67 | + | subc := bindProtocol(t, c, "wl_subcompositor") | |
| 68 | + | child := c.AllocateID() | |
| 69 | + | registerProtocol(t, c, child) | |
| 70 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child) | |
| 71 | + | sub := c.AllocateID() | |
| 72 | + | registerProtocol(t, c, sub) | |
| 73 | + | requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root) | |
| 74 | + | // surfaceMapper owns a one-pixel buffer; construct a second one for the child. | |
| 75 | + | shm := bindProtocol(t, c, "wl_shm") | |
| 76 | + | registerProtocol(t, c, shm) | |
| 77 | + | fd, err := unix.MemfdCreate("input-region-buffer", 0) | |
| 78 | + | if err != nil { | |
| 79 | + | t.Fatal(err) | |
| 80 | + | } | |
| 81 | + | defer unix.Close(fd) | |
| 82 | + | if err := unix.Ftruncate(fd, 64); err != nil { | |
| 83 | + | t.Fatal(err) | |
| 84 | + | } | |
| 85 | + | pool, buffer := c.AllocateID(), c.AllocateID() | |
| 86 | + | registerProtocol(t, c, pool) | |
| 87 | + | registerProtocol(t, c, buffer) | |
| 88 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(64)); err != nil { | |
| 89 | + | t.Fatal(err) | |
| 90 | + | } | |
| 91 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(4), int32(4), int32(16), uint32(0)) | |
| 92 | + | requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(3), int32(4)) | |
| 93 | + | requestProtocol(t, c, root, wayland.SurfaceRequestCommit) | |
| 94 | + | requestProtocol(t, c, child, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |
| 95 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |
| 96 | + | if err := c.Roundtrip(); err != nil { | |
| 97 | + | t.Fatal(err) | |
| 98 | + | } | |
| 99 | + | got := inputChanged(t, events) | |
| 100 | + | if len(got.Rects) == 1 { | |
| 101 | + | got = inputChanged(t, events) | |
| 102 | + | } | |
| 103 | + | if got.ID != w.ID || len(got.Rects) != 2 || got.Rects[1] != (ports.Rect{X: 3, Y: 4, W: 4, H: 4}) { | |
| 104 | + | t.Fatalf("default child input: %+v", got) | |
| 105 | + | } | |
| 106 | + | reg := c.AllocateID() | |
| 107 | + | registerProtocol(t, c, reg) | |
| 108 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, reg) | |
| 109 | + | requestProtocol(t, c, reg, wayland.RegionRequestAdd, int32(1), int32(1), int32(9), int32(9)) | |
| 110 | + | requestProtocol(t, c, child, wayland.SurfaceRequestSetInputRegion, reg) | |
| 111 | + | requestProtocol(t, c, reg, wayland.RegionRequestDestroy) | |
| 112 | + | if err := c.Roundtrip(); err != nil { | |
| 113 | + | t.Fatal(err) | |
| 114 | + | } | |
| 115 | + | // No parent commit is needed, but the child's own commit is required. | |
| 116 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |
| 117 | + | if err := c.Roundtrip(); err != nil { | |
| 118 | + | t.Fatal(err) | |
| 119 | + | } | |
| 120 | + | got = inputChanged(t, events) | |
| 121 | + | if got.ID != w.ID || len(got.Rects) != 2 || got.Rects[1] != (ports.Rect{X: 4, Y: 5, W: 3, H: 3}) { | |
| 122 | + | t.Fatalf("committed child input: %+v", got) | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | func TestConstraintIntersectsInputRegion(t *testing.T) { | |
| 127 | + | s, events, commands, dir := lifecycleServer(t) | |
| 128 | + | c := protocolClient(t, s, dir) | |
| 129 | + | seat := bindProtocol(t, c, "wl_seat") | |
| 130 | + | registerProtocol(t, c, seat) | |
| 131 | + | pointer := c.AllocateID() | |
| 132 | + | registerProtocol(t, c, pointer) | |
| 133 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |
| 134 | + | w, surf, _ := surfaceMapper(t, c, events)() | |
| 135 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 136 | + | region := c.AllocateID() | |
| 137 | + | registerProtocol(t, c, region) | |
| 138 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |
| 139 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1)) | |
| 140 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, region) | |
| 141 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 142 | + | if err := c.Roundtrip(); err != nil { | |
| 143 | + | t.Fatal(err) | |
| 144 | + | } | |
| 145 | + | _ = inputChanged(t, events) | |
| 146 | + | constraints := bindProtocol(t, c, "zwp_pointer_constraints_v1") | |
| 147 | + | confine := c.AllocateID() | |
| 148 | + | registerProtocol(t, c, confine) | |
| 149 | + | requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |
| 150 | + | commands <- ports.FocusWindow{ID: w.ID} | |
| 151 | + | commands <- ports.PointerFocus{ID: w.ID} | |
| 152 | + | commands <- ports.PointerMotionTo{ID: w.ID, X: 0, Y: 0} | |
| 153 | + | if err := c.Roundtrip(); err != nil { | |
| 154 | + | t.Fatal(err) | |
| 155 | + | } | |
| 156 | + | if got := constrained(t, events); got.Rect != (ports.Rect{W: 1, H: 1}) { | |
| 157 | + | t.Fatalf("constraint intersection: %+v", got) | |
| 158 | + | } | |
| 159 | + | } | |
| @@ -0,0 +1,159 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/purego-libwayland/protocol/pointerconstraints" | |||
| 5 | + | "golang.org/x/sys/unix" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 10 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | func inputChanged(t *testing.T, events <-chan ports.ClientEvent) ports.InputRegionChanged { | |||
| 14 | + | t.Helper() | |||
| 15 | + | for { | |||
| 16 | + | if v, ok := waitEvent(t, events, 2*time.Second).(ports.InputRegionChanged); ok { | |||
| 17 | + | return v | |||
| 18 | + | } | |||
| 19 | + | } | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | func TestInputRegionCommitCopySubtractAndReset(t *testing.T) { | |||
| 23 | + | s, events, _, dir := lifecycleServer(t) | |||
| 24 | + | c := protocolClient(t, s, dir) | |||
| 25 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 26 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 27 | + | reg := c.AllocateID() | |||
| 28 | + | registerProtocol(t, c, reg) | |||
| 29 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, reg) | |||
| 30 | + | requestProtocol(t, c, reg, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1)) | |||
| 31 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, reg) | |||
| 32 | + | requestProtocol(t, c, reg, wayland.RegionRequestSubtract, int32(0), int32(0), int32(1), int32(1)) | |||
| 33 | + | requestProtocol(t, c, reg, wayland.RegionRequestDestroy) | |||
| 34 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 35 | + | if got := inputChanged(t, events); got.ID != w.ID || len(got.Rects) != 1 || got.Rects[0] != (ports.Rect{W: 1, H: 1}) { | |||
| 36 | + | t.Fatalf("copied region: %+v", got) | |||
| 37 | + | } | |||
| 38 | + | empty := c.AllocateID() | |||
| 39 | + | registerProtocol(t, c, empty) | |||
| 40 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, empty) | |||
| 41 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, empty) | |||
| 42 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 43 | + | if got := inputChanged(t, events); got.All || len(got.Rects) != 0 { | |||
| 44 | + | t.Fatalf("empty region: %+v", got) | |||
| 45 | + | } | |||
| 46 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, uint32(0)) | |||
| 47 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 48 | + | if got := inputChanged(t, events); !got.All { | |||
| 49 | + | t.Fatalf("reset region: %+v", got) | |||
| 50 | + | } | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | func TestRegionSubtractSplits(t *testing.T) { | |||
| 54 | + | g := ®ion{} | |||
| 55 | + | g.Add(nil, 0, 0, 10, 10) | |||
| 56 | + | g.Subtract(nil, 2, 2, 6, 6) | |||
| 57 | + | if len(g.rects) != 4 || g.boxRect() != (ports.Rect{W: 10, H: 10}) { | |||
| 58 | + | t.Fatalf("subtract: %+v", g.rects) | |||
| 59 | + | } | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | func TestSubsurfaceInputRegionOwnCommitAndOffset(t *testing.T) { | |||
| 63 | + | s, events, _, dir := lifecycleServer(t) | |||
| 64 | + | c := protocolClient(t, s, dir) | |||
| 65 | + | w, root, _ := surfaceMapper(t, c, events)() | |||
| 66 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 67 | + | subc := bindProtocol(t, c, "wl_subcompositor") | |||
| 68 | + | child := c.AllocateID() | |||
| 69 | + | registerProtocol(t, c, child) | |||
| 70 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child) | |||
| 71 | + | sub := c.AllocateID() | |||
| 72 | + | registerProtocol(t, c, sub) | |||
| 73 | + | requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root) | |||
| 74 | + | // surfaceMapper owns a one-pixel buffer; construct a second one for the child. | |||
| 75 | + | shm := bindProtocol(t, c, "wl_shm") | |||
| 76 | + | registerProtocol(t, c, shm) | |||
| 77 | + | fd, err := unix.MemfdCreate("input-region-buffer", 0) | |||
| 78 | + | if err != nil { | |||
| 79 | + | t.Fatal(err) | |||
| 80 | + | } | |||
| 81 | + | defer unix.Close(fd) | |||
| 82 | + | if err := unix.Ftruncate(fd, 64); err != nil { | |||
| 83 | + | t.Fatal(err) | |||
| 84 | + | } | |||
| 85 | + | pool, buffer := c.AllocateID(), c.AllocateID() | |||
| 86 | + | registerProtocol(t, c, pool) | |||
| 87 | + | registerProtocol(t, c, buffer) | |||
| 88 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(64)); err != nil { | |||
| 89 | + | t.Fatal(err) | |||
| 90 | + | } | |||
| 91 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(4), int32(4), int32(16), uint32(0)) | |||
| 92 | + | requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(3), int32(4)) | |||
| 93 | + | requestProtocol(t, c, root, wayland.SurfaceRequestCommit) | |||
| 94 | + | requestProtocol(t, c, child, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0)) | |||
| 95 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |||
| 96 | + | if err := c.Roundtrip(); err != nil { | |||
| 97 | + | t.Fatal(err) | |||
| 98 | + | } | |||
| 99 | + | got := inputChanged(t, events) | |||
| 100 | + | if len(got.Rects) == 1 { | |||
| 101 | + | got = inputChanged(t, events) | |||
| 102 | + | } | |||
| 103 | + | if got.ID != w.ID || len(got.Rects) != 2 || got.Rects[1] != (ports.Rect{X: 3, Y: 4, W: 4, H: 4}) { | |||
| 104 | + | t.Fatalf("default child input: %+v", got) | |||
| 105 | + | } | |||
| 106 | + | reg := c.AllocateID() | |||
| 107 | + | registerProtocol(t, c, reg) | |||
| 108 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, reg) | |||
| 109 | + | requestProtocol(t, c, reg, wayland.RegionRequestAdd, int32(1), int32(1), int32(9), int32(9)) | |||
| 110 | + | requestProtocol(t, c, child, wayland.SurfaceRequestSetInputRegion, reg) | |||
| 111 | + | requestProtocol(t, c, reg, wayland.RegionRequestDestroy) | |||
| 112 | + | if err := c.Roundtrip(); err != nil { | |||
| 113 | + | t.Fatal(err) | |||
| 114 | + | } | |||
| 115 | + | // No parent commit is needed, but the child's own commit is required. | |||
| 116 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |||
| 117 | + | if err := c.Roundtrip(); err != nil { | |||
| 118 | + | t.Fatal(err) | |||
| 119 | + | } | |||
| 120 | + | got = inputChanged(t, events) | |||
| 121 | + | if got.ID != w.ID || len(got.Rects) != 2 || got.Rects[1] != (ports.Rect{X: 4, Y: 5, W: 3, H: 3}) { | |||
| 122 | + | t.Fatalf("committed child input: %+v", got) | |||
| 123 | + | } | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | func TestConstraintIntersectsInputRegion(t *testing.T) { | |||
| 127 | + | s, events, commands, dir := lifecycleServer(t) | |||
| 128 | + | c := protocolClient(t, s, dir) | |||
| 129 | + | seat := bindProtocol(t, c, "wl_seat") | |||
| 130 | + | registerProtocol(t, c, seat) | |||
| 131 | + | pointer := c.AllocateID() | |||
| 132 | + | registerProtocol(t, c, pointer) | |||
| 133 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 134 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 135 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 136 | + | region := c.AllocateID() | |||
| 137 | + | registerProtocol(t, c, region) | |||
| 138 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |||
| 139 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(1), int32(1)) | |||
| 140 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestSetInputRegion, region) | |||
| 141 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 142 | + | if err := c.Roundtrip(); err != nil { | |||
| 143 | + | t.Fatal(err) | |||
| 144 | + | } | |||
| 145 | + | _ = inputChanged(t, events) | |||
| 146 | + | constraints := bindProtocol(t, c, "zwp_pointer_constraints_v1") | |||
| 147 | + | confine := c.AllocateID() | |||
| 148 | + | registerProtocol(t, c, confine) | |||
| 149 | + | requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |||
| 150 | + | commands <- ports.FocusWindow{ID: w.ID} | |||
| 151 | + | commands <- ports.PointerFocus{ID: w.ID} | |||
| 152 | + | commands <- ports.PointerMotionTo{ID: w.ID, X: 0, Y: 0} | |||
| 153 | + | if err := c.Roundtrip(); err != nil { | |||
| 154 | + | t.Fatal(err) | |||
| 155 | + | } | |||
| 156 | + | if got := constrained(t, events); got.Rect != (ports.Rect{W: 1, H: 1}) { | |||
| 157 | + | t.Fatalf("constraint intersection: %+v", got) | |||
| 158 | + | } | |||
| 159 | + | } | |||
M internal/adapters/wayland/surface.go +14 -4
| @@ -43,9 +43,13 @@ type surface struct { | |||
| 43 | 43 | scale float64 | |
| 44 | 44 | // content is the last content built from this surface's own buffer; | |
| 45 | 45 | // has is false while no buffer is attached. | |
| 46 | - | content ports.SurfaceContent | |
| 47 | - | has bool | |
| 48 | - | sub subState | |
| 46 | + | content ports.SurfaceContent | |
| 47 | + | has bool | |
| 48 | + | sub subState | |
| 49 | + | inputAll, pendingInputAll, pendingInputSet bool | |
| 50 | + | inputRects, pendingInputRects []ports.Rect | |
| 51 | + | sentInput, lastInputAll bool | |
| 52 | + | lastInputRects []ports.Rect | |
| 49 | 53 | // tearing is the surface's wp_tearing_control_v1; async is the | |
| 50 | 54 | // committed hint, pendingAsync the requested one. | |
| 51 | 55 | tearing *tearingHandler | |
| @@ -200,6 +204,7 @@ func (s *surface) detach() { | |||
| 200 | 204 | } | |
| 201 | 205 | s.sub.parent = nil | |
| 202 | 206 | p.redraw() | |
| 207 | + | p.emitInput() | |
| 203 | 208 | s.sendScale() | |
| 204 | 209 | } | |
| 205 | 210 | ||
| @@ -312,6 +317,11 @@ func (s *surface) applyCommit() { | |||
| 312 | 317 | s.attached = false | |
| 313 | 318 | } | |
| 314 | 319 | s.applySync(cs) | |
| 320 | + | if s.pendingInputSet { | |
| 321 | + | s.inputAll, s.inputRects = s.pendingInputAll, s.pendingInputRects | |
| 322 | + | s.pendingInputRects = nil | |
| 323 | + | s.pendingInputSet = false | |
| 324 | + | } | |
| 315 | 325 | if len(s.callbacks) > 0 { | |
| 316 | 326 | s.server.queueFrames(s.server.frameOutput(s), s.callbacks) | |
| 317 | 327 | } | |
| @@ -357,6 +367,7 @@ func (s *surface) applyCommit() { | |||
| 357 | 367 | s.xdg.geometry, geometry = s.xdg.pendingGeometry, true | |
| 358 | 368 | } | |
| 359 | 369 | s.commitConstraint(geometry) | |
| 370 | + | s.emitInput() | |
| 360 | 371 | if s.xdg != nil && s.xdg.window != nil { | |
| 361 | 372 | s.xdg.window.afterCommit() | |
| 362 | 373 | } | |
| @@ -421,7 +432,6 @@ func (s *surface) commitDamage(fresh, resized bool, bw, bh int) { | |||
| 421 | 432 | s.committed = d | |
| 422 | 433 | } | |
| 423 | 434 | func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {} | |
| 424 | - | func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {} | |
| 425 | 435 | func (*surface) SetBufferTransform(*wayland.Surface, int32) {} | |
| 426 | 436 | func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | |
| 427 | 437 | if v < 1 { | |
| @@ -43,9 +43,13 @@ type surface struct { | |||||
| 43 | scale float64 | 43 | scale float64 | ||
| 44 | // content is the last content built from this surface's own buffer; | 44 | // content is the last content built from this surface's own buffer; | ||
| 45 | // has is false while no buffer is attached. | 45 | // has is false while no buffer is attached. | ||
| 46 | - | content ports.SurfaceContent | 46 | + | content ports.SurfaceContent |
| 47 | - | has bool | 47 | + | has bool |
| 48 | - | sub subState | 48 | + | sub subState |
| 49 | + | inputAll, pendingInputAll, pendingInputSet bool | |||
| 50 | + | inputRects, pendingInputRects []ports.Rect | |||
| 51 | + | sentInput, lastInputAll bool | |||
| 52 | + | lastInputRects []ports.Rect | |||
| 49 | // tearing is the surface's wp_tearing_control_v1; async is the | 53 | // tearing is the surface's wp_tearing_control_v1; async is the | ||
| 50 | // committed hint, pendingAsync the requested one. | 54 | // committed hint, pendingAsync the requested one. | ||
| 51 | tearing *tearingHandler | 55 | tearing *tearingHandler | ||
| @@ -200,6 +204,7 @@ func (s *surface) detach() { | |||||
| 200 | } | 204 | } | ||
| 201 | s.sub.parent = nil | 205 | s.sub.parent = nil | ||
| 202 | p.redraw() | 206 | p.redraw() | ||
| 207 | + | p.emitInput() | |||
| 203 | s.sendScale() | 208 | s.sendScale() | ||
| 204 | } | 209 | } | ||
| 205 | 210 | ||||
| @@ -312,6 +317,11 @@ func (s *surface) applyCommit() { | |||||
| 312 | s.attached = false | 317 | s.attached = false | ||
| 313 | } | 318 | } | ||
| 314 | s.applySync(cs) | 319 | s.applySync(cs) | ||
| 320 | + | if s.pendingInputSet { | |||
| 321 | + | s.inputAll, s.inputRects = s.pendingInputAll, s.pendingInputRects | |||
| 322 | + | s.pendingInputRects = nil | |||
| 323 | + | s.pendingInputSet = false | |||
| 324 | + | } | |||
| 315 | if len(s.callbacks) > 0 { | 325 | if len(s.callbacks) > 0 { | ||
| 316 | s.server.queueFrames(s.server.frameOutput(s), s.callbacks) | 326 | s.server.queueFrames(s.server.frameOutput(s), s.callbacks) | ||
| 317 | } | 327 | } | ||
| @@ -357,6 +367,7 @@ func (s *surface) applyCommit() { | |||||
| 357 | s.xdg.geometry, geometry = s.xdg.pendingGeometry, true | 367 | s.xdg.geometry, geometry = s.xdg.pendingGeometry, true | ||
| 358 | } | 368 | } | ||
| 359 | s.commitConstraint(geometry) | 369 | s.commitConstraint(geometry) | ||
| 370 | + | s.emitInput() | |||
| 360 | if s.xdg != nil && s.xdg.window != nil { | 371 | if s.xdg != nil && s.xdg.window != nil { | ||
| 361 | s.xdg.window.afterCommit() | 372 | s.xdg.window.afterCommit() | ||
| 362 | } | 373 | } | ||
| @@ -421,7 +432,6 @@ func (s *surface) commitDamage(fresh, resized bool, bw, bh int) { | |||||
| 421 | s.committed = d | 432 | s.committed = d | ||
| 422 | } | 433 | } | ||
| 423 | func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {} | 434 | func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {} | ||
| 424 | - | func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {} | |||
| 425 | func (*surface) SetBufferTransform(*wayland.Surface, int32) {} | 435 | func (*surface) SetBufferTransform(*wayland.Surface, int32) {} | ||
| 426 | func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | 436 | func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | ||
| 427 | if v < 1 { | 437 | if v < 1 { | ||
M internal/core/core.go +25 -5
| @@ -77,8 +77,9 @@ type Core struct { | |||
| 77 | 77 | // token, until their window maps. | |
| 78 | 78 | terms map[string]*termSpawn | |
| 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 | |
| @@ -287,7 +288,7 @@ func New(cfg ports.Config, ch Channels) (*Core, error) { | |||
| 287 | 288 | return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1") | |
| 288 | 289 | } | |
| 289 | 290 | // A placeholder screen holds windows until the first output arrives. | |
| 290 | - | 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{}} | |
| 291 | + | 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{}} | |
| 291 | 292 | if err := c.apply(cfg); err != nil { | |
| 292 | 293 | return nil, err | |
| 293 | 294 | } | |
| @@ -564,6 +565,19 @@ func (c *Core) allLayers() []ports.LayerSurface { | |||
| 564 | 565 | return all | |
| 565 | 566 | } | |
| 566 | 567 | ||
| 568 | + | func (c *Core) acceptsInput(id WindowID, x, y float64) bool { | |
| 569 | + | r, ok := c.inputRegions[id] | |
| 570 | + | if !ok || r.All { | |
| 571 | + | return true | |
| 572 | + | } | |
| 573 | + | for _, box := range r.Rects { | |
| 574 | + | if x >= float64(box.X) && x < float64(box.X+box.W) && y >= float64(box.Y) && y < float64(box.Y+box.H) { | |
| 575 | + | return true | |
| 576 | + | } | |
| 577 | + | } | |
| 578 | + | return false | |
| 579 | + | } | |
| 580 | + | ||
| 567 | 581 | // hit returns the surface under the global logical point and the point in | |
| 568 | 582 | // its surface coordinates: popups, then overlay and top layers, then | |
| 569 | 583 | // windows (fullscreen wins; otherwise the last visible placement is | |
| @@ -580,7 +594,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||
| 580 | 594 | if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 { | |
| 581 | 595 | return id, px, py | |
| 582 | 596 | } | |
| 583 | - | if id, px, py := layerAt(sc, lx, ly, true); id != 0 { | |
| 597 | + | if id, px, py := c.layerAt(sc, lx, ly, true); id != 0 { | |
| 584 | 598 | return id, px, py | |
| 585 | 599 | } | |
| 586 | 600 | if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 { | |
| @@ -597,6 +611,9 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||
| 597 | 611 | if full && !p.Floating { | |
| 598 | 612 | continue | |
| 599 | 613 | } | |
| 614 | + | if !c.acceptsInput(p.ID, lx-float64(r.X), ly-float64(r.Y)) { | |
| 615 | + | continue | |
| 616 | + | } | |
| 600 | 617 | if id == 0 || p.Fullscreen || p.Floating { | |
| 601 | 618 | id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y) | |
| 602 | 619 | } | |
| @@ -604,7 +621,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||
| 604 | 621 | } | |
| 605 | 622 | } | |
| 606 | 623 | if id == 0 { | |
| 607 | - | return layerAt(sc, lx, ly, false) | |
| 624 | + | return c.layerAt(sc, lx, ly, false) | |
| 608 | 625 | } | |
| 609 | 626 | return id, sx, sy | |
| 610 | 627 | } | |
| @@ -636,6 +653,8 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 636 | 653 | case ports.LayerChanged: | |
| 637 | 654 | c.layerChanged = true | |
| 638 | 655 | c.setLayers(v.Layers) | |
| 656 | + | case ports.InputRegionChanged: | |
| 657 | + | c.inputRegions[v.ID] = v | |
| 639 | 658 | case ports.WindowMapped: | |
| 640 | 659 | c.clients[v.ID] = v | |
| 641 | 660 | if v.Floating { | |
| @@ -696,6 +715,7 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 696 | 715 | return nil | |
| 697 | 716 | } | |
| 698 | 717 | delete(c.clients, v.ID) | |
| 718 | + | delete(c.inputRegions, v.ID) | |
| 699 | 719 | delete(c.inhibitors, v.ID) | |
| 700 | 720 | delete(c.idle, v.ID) | |
| 701 | 721 | if s, _ := c.screenOf(v.ID); s != nil { | |
| @@ -77,8 +77,9 @@ type Core struct { | |||||
| 77 | // token, until their window maps. | 77 | // token, until their window maps. | ||
| 78 | terms map[string]*termSpawn | 78 | terms map[string]*termSpawn | ||
| 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 | ||
| @@ -287,7 +288,7 @@ func New(cfg ports.Config, ch Channels) (*Core, error) { | |||||
| 287 | return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1") | 288 | return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1") | ||
| 288 | } | 289 | } | ||
| 289 | // A placeholder screen holds windows until the first output arrives. | 290 | // A placeholder screen holds windows until the first output arrives. | ||
| 290 | - | 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{}} | 291 | + | 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{}} |
| 291 | if err := c.apply(cfg); err != nil { | 292 | if err := c.apply(cfg); err != nil { | ||
| 292 | return nil, err | 293 | return nil, err | ||
| 293 | } | 294 | } | ||
| @@ -564,6 +565,19 @@ func (c *Core) allLayers() []ports.LayerSurface { | |||||
| 564 | return all | 565 | return all | ||
| 565 | } | 566 | } | ||
| 566 | 567 | ||||
| 568 | + | func (c *Core) acceptsInput(id WindowID, x, y float64) bool { | |||
| 569 | + | r, ok := c.inputRegions[id] | |||
| 570 | + | if !ok || r.All { | |||
| 571 | + | return true | |||
| 572 | + | } | |||
| 573 | + | for _, box := range r.Rects { | |||
| 574 | + | if x >= float64(box.X) && x < float64(box.X+box.W) && y >= float64(box.Y) && y < float64(box.Y+box.H) { | |||
| 575 | + | return true | |||
| 576 | + | } | |||
| 577 | + | } | |||
| 578 | + | return false | |||
| 579 | + | } | |||
| 580 | + | ||||
| 567 | // hit returns the surface under the global logical point and the point in | 581 | // hit returns the surface under the global logical point and the point in | ||
| 568 | // its surface coordinates: popups, then overlay and top layers, then | 582 | // its surface coordinates: popups, then overlay and top layers, then | ||
| 569 | // windows (fullscreen wins; otherwise the last visible placement is | 583 | // windows (fullscreen wins; otherwise the last visible placement is | ||
| @@ -580,7 +594,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||||
| 580 | if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 { | 594 | if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 { | ||
| 581 | return id, px, py | 595 | return id, px, py | ||
| 582 | } | 596 | } | ||
| 583 | - | if id, px, py := layerAt(sc, lx, ly, true); id != 0 { | 597 | + | if id, px, py := c.layerAt(sc, lx, ly, true); id != 0 { |
| 584 | return id, px, py | 598 | return id, px, py | ||
| 585 | } | 599 | } | ||
| 586 | if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 { | 600 | if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 { | ||
| @@ -597,6 +611,9 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||||
| 597 | if full && !p.Floating { | 611 | if full && !p.Floating { | ||
| 598 | continue | 612 | continue | ||
| 599 | } | 613 | } | ||
| 614 | + | if !c.acceptsInput(p.ID, lx-float64(r.X), ly-float64(r.Y)) { | |||
| 615 | + | continue | |||
| 616 | + | } | |||
| 600 | if id == 0 || p.Fullscreen || p.Floating { | 617 | if id == 0 || p.Fullscreen || p.Floating { | ||
| 601 | id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y) | 618 | id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y) | ||
| 602 | } | 619 | } | ||
| @@ -604,7 +621,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||||
| 604 | } | 621 | } | ||
| 605 | } | 622 | } | ||
| 606 | if id == 0 { | 623 | if id == 0 { | ||
| 607 | - | return layerAt(sc, lx, ly, false) | 624 | + | return c.layerAt(sc, lx, ly, false) |
| 608 | } | 625 | } | ||
| 609 | return id, sx, sy | 626 | return id, sx, sy | ||
| 610 | } | 627 | } | ||
| @@ -636,6 +653,8 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 636 | case ports.LayerChanged: | 653 | case ports.LayerChanged: | ||
| 637 | c.layerChanged = true | 654 | c.layerChanged = true | ||
| 638 | c.setLayers(v.Layers) | 655 | c.setLayers(v.Layers) | ||
| 656 | + | case ports.InputRegionChanged: | |||
| 657 | + | c.inputRegions[v.ID] = v | |||
| 639 | case ports.WindowMapped: | 658 | case ports.WindowMapped: | ||
| 640 | c.clients[v.ID] = v | 659 | c.clients[v.ID] = v | ||
| 641 | if v.Floating { | 660 | if v.Floating { | ||
| @@ -696,6 +715,7 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 696 | return nil | 715 | return nil | ||
| 697 | } | 716 | } | ||
| 698 | delete(c.clients, v.ID) | 717 | delete(c.clients, v.ID) | ||
| 718 | + | delete(c.inputRegions, v.ID) | |||
| 699 | delete(c.inhibitors, v.ID) | 719 | delete(c.inhibitors, v.ID) | ||
| 700 | delete(c.idle, v.ID) | 720 | delete(c.idle, v.ID) | ||
| 701 | if s, _ := c.screenOf(v.ID); s != nil { | 721 | if s, _ := c.screenOf(v.ID); s != nil { | ||
M internal/core/layer_input_test.go +46 -0
| @@ -269,3 +269,49 @@ 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 | + | } | |
| @@ -269,3 +269,49 @@ 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 | + | } | |||
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 | ||||