detached 75e0f9c

Merge pull request #20: fix(wayland): honour wl_surface.set_input_region

# Conflicts:
#	internal/adapters/wayland/fifo.go
#	internal/adapters/wayland/surface.go
commit
75e0f9c09e5ed7ec4573dd7f9539f4a7fd091291
author
brice · · 2026-09-27 14:26 UTC
merge of
4e1cc2a + adaacbc · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
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 {
138138 return ports.Rect{}
139139 }
140140 if g := s.regions[reg.Resource]; g != nil {
141- return g.box
141+ return g.boxRect()
142142 }
143143 return ports.Rect{}
144144 }
@@ -205,6 +205,26 @@ func (s *Server) dropConstraint(c *constraint) {
205205 s.updateConstraint()
206206 }
207207
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+
208228 // updateConstraint activates the constraint of the window holding both
209229 // pointer and keyboard focus, and deactivates any other.
210230 func (s *Server) updateConstraint() {
@@ -217,23 +237,11 @@ func (s *Server) updateConstraint() {
217237 if want == s.constraint {
218238 return
219239 }
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()
233241 if want == nil {
234- s.emit(ports.PointerConstrained{})
235242 return
236243 }
244+ s.constraint = want
237245 want.active = true
238246 if want.lock != nil {
239247 want.lock.SendLocked()
@@ -258,6 +266,28 @@ func (s *Server) emitConstraint(c *constraint) {
258266 r.X -= x.geometry.X
259267 r.Y -= x.geometry.Y
260268 }
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+ }
261291 s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}})
262292 }
263293
@@ -265,10 +295,24 @@ func (s *Server) emitConstraint(c *constraint) {
265295 // activates only there, so it never warps the pointer.
266296 func (c *constraint) contains(w *window) bool {
267297 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+ }
268313 if r.W <= 0 || r.H <= 0 {
269314 return true
270315 }
271- x, y := w.surfacePoint(w.xdg.server.pointerX, w.xdg.server.pointerY)
272316 return x >= float64(r.X) && x < float64(r.X+r.W) && y >= float64(r.Y) && y < float64(r.Y+r.H)
273317 }
274318
@@ -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.box141+ 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 both228 // 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 return238 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 return242 return
236 }243 }
244+ s.constraint = want
237 want.active = true245 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.X266 r.X -= x.geometry.X
259 r.Y -= x.geometry.Y267 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.region297 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 true314 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 {
198198
199199 // update is a surface's pending state taken out by a commit that waits.
200200 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
234236 }
235237
236238 type childLayout struct {
@@ -266,6 +268,9 @@ func sameLayout(a, b []childLayout) bool {
266268 // cleared, sticky state (scale, hints, viewport, geometry, layer) kept.
267269 func (s *surface) takePending() update {
268270 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
269274 s.attached, s.pending, s.callbacks = false, nil, nil
270275 s.pendingFeedback, s.pendingSync = nil, nil
271276 s.pendingDamage, s.pendingBufDamage = nil, nil
@@ -289,6 +294,7 @@ func (s *surface) takePending() update {
289294 // putPending makes an update the surface's pending state again. Parts
290295 // whose object is gone (viewport, child, role, constraint) are dropped.
291296 func (s *surface) putPending(u update) {
297+ s.pendingInputSet, s.pendingInputAll, s.pendingInputRects = u.inputSet, u.inputAll, u.inputRects
292298 s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks
293299 s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind
294300 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 bool201+ inputSet, inputAll bool
202- buffer *wayland.Buffer202+ inputRects []ports.Rect
203- scale int203+ attached bool
204- async bool204+ buffer *wayland.Buffer
205- kind uint32205+ scale int
206- callbacks []*wayland.Callback206+ async bool
207- vp *viewport207+ kind uint32
208- vpW, vpH int32208+ callbacks []*wayland.Callback
209- vpSet bool209+ vp *viewport
210- vpSrc [4]server.Fixed210+ vpW, vpH int32
211- vpCrop bool211+ vpSet bool
212- layout []childLayout212+ vpSrc [4]server.Fixed
213- deps []*update213+ vpCrop bool
214- owner *surface214+ layout []childLayout
215- prev *update215+ deps []*update
216- synced, bound bool216+ owner *surface
217- xdg *xdgSurface217+ prev *update
218- geometry ports.Rect218+ synced, bound bool
219- cons *constraint219+ xdg *xdgSurface
220- region *ports.Rect220+ geometry ports.Rect
221- layer *layerSurface221+ cons *constraint
222- layerNext layerState222+ region *ports.Rect
223- barrier bool223+ layer *layerSurface
224- wait bool224+ layerNext layerState
225- at time.Time225+ barrier bool
226- feedback []*presentationtime.WpPresentationFeedback226+ wait bool
227- sync *commitSync227+ at time.Time
228- color SurfaceColor228+ feedback []*presentationtime.WpPresentationFeedback
229- representation surfaceRepresentation229+ sync *commitSync
230- damage []ports.Rect230+ color SurfaceColor
231- bufDamage []ports.Rect231+ representation surfaceRepresentation
232- readyGeneration uint64232+ damage []ports.Rect
233- readyResult bool233+ 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, nil274 s.attached, s.pending, s.callbacks = false, nil, nil
270 s.pendingFeedback, s.pendingSync = nil, nil275 s.pendingFeedback, s.pendingSync = nil, nil
271 s.pendingDamage, s.pendingBufDamage = nil, nil276 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. Parts294 // 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.callbacks298 s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks
293 s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind299 s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind
294 s.pendingColor = u.color300 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 {
7373 type compositor struct{ server *Server }
7474
7575 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}
7777 if w, err := wayland.NewSurface(r.Client(), r.Version(), id, state); err == nil {
7878 c.server.nextSurface++
7979 state.identity = c.server.nextSurface
@@ -125,6 +125,7 @@ func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, par
125125 up.sub.pendingLayout = append(append([]childLayout(nil), up.sub.pendingLayout...), childLayout{child: state})
126126 // The child follows its root window's output and scale.
127127 state.sendScale()
128+ up.emitInput()
128129 sub.OnDestroy = func() {
129130 if state.sub.role != sub {
130131 return
@@ -218,23 +219,40 @@ func (s subsurface) restack(r *wayland.Subsurface, sibling *wayland.Surface, abo
218219 p.sub.pendingLayout = list
219220 }
220221
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 }
224224
225225 func (*region) Destroy(*wayland.Region) {}
226226 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) {
227232 if w <= 0 || h <= 0 {
228233 return
229234 }
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+ }
234253 }
235- g.box = r
254+ g.rects = out
236255 }
237-func (*region) Subtract(*wayland.Region, int32, int32, int32, int32) {}
238256
239257 type shm struct{ server *Server }
240258
@@ -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.nextSurface79 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 return131 return
@@ -218,23 +219,40 @@ func (s subsurface) restack(r *wayland.Subsurface, sibling *wayland.Surface, abo
218 p.sub.pendingLayout = list219 p.sub.pendingLayout = list
219 }220 }
220 221
221-// region keeps the bounding box of the rectangles added: pointer222+// 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 return233 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 = r254+ 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 := &region{}
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 := &region{}
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 := &region{}
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 := &region{}
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() {
163163 s.changePointerFocus(0, 0, 0)
164164 }
165165 l.mapped = false
166+ l.surface.resetInputEmission()
166167 l.configured = false
167168 l.acked = false
168169 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 = false165 l.mapped = false
166+ l.surface.resetInputEmission()
166 l.configured = false167 l.configured = false
167 l.acked = false168 l.acked = false
168 l.serials = nil169 l.serials = nil
M internal/adapters/wayland/surface.go +15 -5
@@ -45,10 +45,14 @@ type surface struct {
4545 scale float64
4646 // content is the last content built from this surface's own buffer;
4747 // 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
5256 // cachedTree is never modified once returned to emitContent.
5357 cachedTree []ports.Subsurface
5458 treeDirty bool
@@ -233,6 +237,7 @@ func (s *surface) detach() {
233237 s.dropQueue()
234238 s.flushDesync()
235239 p.redraw()
240+ p.emitInput()
236241 s.sendScale()
237242 }
238243
@@ -359,6 +364,11 @@ func (s *surface) applyCommit() {
359364 s.attached = false
360365 }
361366 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+ }
362372 if len(s.callbacks) > 0 {
363373 s.server.queueFrames(s.server.frameOutput(s), s.callbacks)
364374 }
@@ -423,6 +433,7 @@ func (s *surface) applyCommit() {
423433 s.xdg.geometry, geometry = s.xdg.pendingGeometry, true
424434 }
425435 s.commitConstraint(geometry)
436+ s.emitInput()
426437 if s.xdg != nil && s.xdg.window != nil {
427438 s.xdg.window.afterCommit()
428439 }
@@ -500,7 +511,6 @@ func (s *surface) commitDamage(fresh, resized bool, bw, bh int) {
500511 s.committed = d
501512 }
502513 func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {}
503-func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {}
504514 func (*surface) SetBufferTransform(*wayland.Surface, int32) {}
505515 func (s *surface) SetBufferScale(r *wayland.Surface, v int32) {
506516 if v < 1 {
@@ -45,10 +45,14 @@ type surface struct {
45 scale float6445 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.SurfaceContent48+ inputAll, pendingInputAll, pendingInputSet bool
49- has bool49+ inputRects, pendingInputRects []ports.Rect
50- identity uint6450+ sentInput, lastInputAll bool
51- version uint6451+ 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.Subsurface57 cachedTree []ports.Subsurface
54 treeDirty bool58 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 = false364 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, true433 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 = d511 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() {
178178 return
179179 }
180180 w.mapped = false
181+ w.xdg.surface.resetInputEmission()
181182 w.xdg.configured = false
182183 w.xdg.acked = false
183184 w.xdg.serials = nil
@@ -178,6 +178,7 @@ func (w *window) unmap() {
178 return178 return
179 }179 }
180 w.mapped = false180 w.mapped = false
181+ w.xdg.surface.resetInputEmission()
181 w.xdg.configured = false182 w.xdg.configured = false
182 w.xdg.acked = false183 w.xdg.acked = false
183 w.xdg.serials = nil184 w.xdg.serials = nil
M internal/core/core.go +33 -5
@@ -77,8 +77,9 @@ type Core struct {
7777 terms map[string]*termSpawn
7878 firstTerminalResolved bool
7979 // 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
8283 // popups are the placed xdg_popups; popupOrder stacks them, oldest first.
8384 popups map[WindowID]*popupState
8485 popupOrder []WindowID
@@ -288,7 +289,7 @@ func New(cfg ports.Config, ch Channels) (*Core, error) {
288289 return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1")
289290 }
290291 // 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{}}
292293 if err := c.apply(cfg); err != nil {
293294 return nil, err
294295 }
@@ -361,16 +362,24 @@ func (c *Core) keyboardFocus() WindowID {
361362 // setLayers splits the mapped layer surfaces by output. A surface without an
362363 // output, or on an unknown one, goes to the focused output.
363364 func (c *Core) setLayers(all []ports.LayerSurface) {
365+ previous := make(map[WindowID]bool)
364366 for _, sc := range c.screens {
367+ for _, l := range sc.layers {
368+ previous[l.ID] = true
369+ }
365370 sc.layers = nil
366371 }
367372 for _, l := range all {
373+ delete(previous, l.ID)
368374 sc := c.cur()
369375 if i := c.screenIndex(l.Output); i >= 0 {
370376 sc = c.screens[i]
371377 }
372378 sc.layers = append(sc.layers, l)
373379 }
380+ for id := range previous {
381+ delete(c.inputRegions, id)
382+ }
374383 for _, sc := range c.screens {
375384 sc.arrange()
376385 }
@@ -565,6 +574,19 @@ func (c *Core) allLayers() []ports.LayerSurface {
565574 return all
566575 }
567576
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+
568590 // hit returns the surface under the global logical point and the point in
569591 // its surface coordinates: popups, then overlay and top layers, then
570592 // windows (fullscreen wins; otherwise the last visible placement is
@@ -581,7 +603,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
581603 if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
582604 return id, px, py
583605 }
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 {
585607 return id, px, py
586608 }
587609 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) {
598620 if full && !p.Floating {
599621 continue
600622 }
623+ if !c.acceptsInput(p.ID, lx-float64(r.X), ly-float64(r.Y)) {
624+ continue
625+ }
601626 if id == 0 || p.Fullscreen || p.Floating {
602627 id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y)
603628 }
@@ -605,7 +630,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
605630 }
606631 }
607632 if id == 0 {
608- return layerAt(sc, lx, ly, false)
633+ return c.layerAt(sc, lx, ly, false)
609634 }
610635 return id, sx, sy
611636 }
@@ -637,6 +662,8 @@ func (c *Core) Run(ctx context.Context) error {
637662 case ports.LayerChanged:
638663 c.layerChanged = true
639664 c.setLayers(v.Layers)
665+ case ports.InputRegionChanged:
666+ c.inputRegions[v.ID] = v
640667 case ports.WindowMapped:
641668 c.clients[v.ID] = v
642669 if v.Floating {
@@ -697,6 +724,7 @@ func (c *Core) Run(ctx context.Context) error {
697724 return nil
698725 }
699726 delete(c.clients, v.ID)
727+ delete(c.inputRegions, v.ID)
700728 delete(c.inhibitors, v.ID)
701729 delete(c.idle, v.ID)
702730 if s, _ := c.screenOf(v.ID); s != nil {
@@ -77,8 +77,9 @@ type Core struct {
77 terms map[string]*termSpawn77 terms map[string]*termSpawn
78 firstTerminalResolved bool78 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.WindowMapped80+ clients map[WindowID]ports.WindowMapped
81- sentState ports.State81+ 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]*popupState84 popups map[WindowID]*popupState
84 popupOrder []WindowID85 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, err294 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 an362 // 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 = nil370 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 all574 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 in590 // hit returns the surface under the global logical point and the point in
569 // its surface coordinates: popups, then overlay and top layers, then591 // its surface coordinates: popups, then overlay and top layers, then
570 // windows (fullscreen wins; otherwise the last visible placement is592 // 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, py604 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, py607 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 continue621 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, sy635 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 = true663 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] = v668 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 nil724 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) {
269269 t.Fatalf("key to %d, want the new window", v.ID)
270270 }
271271 }
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 {
104104
105105 // layerAt returns the topmost shown layer surface under the output-local
106106 // 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) {
108108 full := hasFullscreen(sc)
109109 for i := len(sc.placed) - 1; i >= 0; i-- {
110110 l := sc.placed[i]
@@ -112,7 +112,7 @@ func layerAt(sc *screen, lx, ly float64, above bool) (WindowID, float64, float64
112112 if (l.Layer >= ports.LayerTop) != above || (full && (l.Layer == ports.LayerBottom || l.Layer == ports.LayerTop)) || r.W <= 0 || r.H <= 0 {
113113 continue
114114 }
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)) {
116116 return l.ID, lx - float64(r.X), ly - float64(r.Y)
117117 }
118118 }
@@ -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-local105 // 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 continue113 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
290290 if !ok || s != sc {
291291 continue
292292 }
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)) {
294294 return p.id, lx - float64(r.X), ly - float64(r.Y)
295295 }
296296 }
@@ -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 continue291 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"
5050
5151 func (WindowMapped) clientEvent() {}
5252
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+
5363 // WindowUnmapped carries wayland → core removal.
5464 type WindowUnmapped struct{ ID WindowID }
5565
@@ -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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close