detached 5278439

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
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 }
@@ -258,6 +258,28 @@ func (s *Server) emitConstraint(c *constraint) {
258258 r.X -= x.geometry.X
259259 r.Y -= x.geometry.Y
260260 }
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+ }
261283 s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}})
262284 }
263285
@@ -265,10 +287,23 @@ func (s *Server) emitConstraint(c *constraint) {
265287 // activates only there, so it never warps the pointer.
266288 func (c *constraint) contains(w *window) bool {
267289 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+ }
268304 if r.W <= 0 || r.H <= 0 {
269305 return true
270306 }
271- x, y := w.surfacePoint(w.xdg.server.pointerX, w.xdg.server.pointerY)
272307 return x >= float64(r.X) && x < float64(r.X+r.W) && y >= float64(r.Y) && y < float64(r.Y+r.H)
273308 }
274309
@@ -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 }
@@ -258,6 +258,28 @@ func (s *Server) emitConstraint(c *constraint) {
258 r.X -= x.geometry.X258 r.X -= x.geometry.X
259 r.Y -= x.geometry.Y259 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.region289 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 true305 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 {
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- 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
226228 }
227229
228230 type childMove struct {
@@ -234,6 +236,9 @@ type childMove struct {
234236 // cleared, sticky state (scale, hints, viewport, geometry, layer) kept.
235237 func (s *surface) takePending() update {
236238 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
237242 s.attached, s.pending, s.callbacks = false, nil, nil
238243 s.pendingFeedback, s.pendingSync = nil, nil
239244 s.pendingDamage, s.pendingBufDamage = nil, nil
@@ -262,6 +267,7 @@ func (s *surface) takePending() update {
262267 // putPending makes an update the surface's pending state again. Parts
263268 // whose object is gone (viewport, child, role, constraint) are dropped.
264269 func (s *surface) putPending(u update) {
270+ s.pendingInputSet, s.pendingInputAll, s.pendingInputRects = u.inputSet, u.inputAll, u.inputRects
265271 s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks
266272 s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind
267273 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 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- moves []childMove210+ vpW, vpH int32
211- xdg *xdgSurface211+ vpSet bool
212- geometry ports.Rect212+ moves []childMove
213- cons *constraint213+ xdg *xdgSurface
214- region *ports.Rect214+ geometry ports.Rect
215- layer *layerSurface215+ cons *constraint
216- layerNext layerState216+ region *ports.Rect
217- barrier bool217+ layer *layerSurface
218- wait bool218+ layerNext layerState
219- at time.Time219+ barrier bool
220- feedback []*presentationtime.WpPresentationFeedback220+ wait bool
221- sync *commitSync221+ at time.Time
222- color SurfaceColor222+ feedback []*presentationtime.WpPresentationFeedback
223- representation surfaceRepresentation223+ sync *commitSync
224- damage []ports.Rect224+ color SurfaceColor
225- bufDamage []ports.Rect225+ 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, nil242 s.attached, s.pending, s.callbacks = false, nil, nil
238 s.pendingFeedback, s.pendingSync = nil, nil243 s.pendingFeedback, s.pendingSync = nil, nil
239 s.pendingDamage, s.pendingBufDamage = nil, nil244 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. Parts267 // 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.callbacks271 s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks
266 s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind272 s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind
267 s.pendingColor = u.color273 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 state.wl = w
7979 c.server.surfaces[w.Resource] = state
@@ -122,6 +122,7 @@ func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, par
122122 up.sub.children = append(up.sub.children, state)
123123 // The child follows its root window's output and scale.
124124 state.sendScale()
125+ up.emitInput()
125126 sub.OnDestroy = func() {
126127 if state.sub.role != sub {
127128 return
@@ -212,23 +213,40 @@ func sortBelowFirst(list []*surface) {
212213 copy(list, append(below, above...))
213214 }
214215
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 }
218218
219219 func (*region) Destroy(*wayland.Region) {}
220220 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) {
221226 if w <= 0 || h <= 0 {
222227 return
223228 }
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+ }
228247 }
229- g.box = r
248+ g.rects = out
230249 }
231-func (*region) Subtract(*wayland.Region, int32, int32, int32, int32) {}
232250
233251 type shm struct{ server *Server }
234252
@@ -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 = w78 state.wl = w
79 c.server.surfaces[w.Resource] = state79 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 return128 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: pointer216+// 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 return227 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 = r248+ 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 := &region{}
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 := &region{}
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 {
4343 scale float64
4444 // content is the last content built from this surface's own buffer;
4545 // 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
4953 // tearing is the surface's wp_tearing_control_v1; async is the
5054 // committed hint, pendingAsync the requested one.
5155 tearing *tearingHandler
@@ -200,6 +204,7 @@ func (s *surface) detach() {
200204 }
201205 s.sub.parent = nil
202206 p.redraw()
207+ p.emitInput()
203208 s.sendScale()
204209 }
205210
@@ -312,6 +317,11 @@ func (s *surface) applyCommit() {
312317 s.attached = false
313318 }
314319 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+ }
315325 if len(s.callbacks) > 0 {
316326 s.server.queueFrames(s.server.frameOutput(s), s.callbacks)
317327 }
@@ -357,6 +367,7 @@ func (s *surface) applyCommit() {
357367 s.xdg.geometry, geometry = s.xdg.pendingGeometry, true
358368 }
359369 s.commitConstraint(geometry)
370+ s.emitInput()
360371 if s.xdg != nil && s.xdg.window != nil {
361372 s.xdg.window.afterCommit()
362373 }
@@ -421,7 +432,6 @@ func (s *surface) commitDamage(fresh, resized bool, bw, bh int) {
421432 s.committed = d
422433 }
423434 func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {}
424-func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {}
425435 func (*surface) SetBufferTransform(*wayland.Surface, int32) {}
426436 func (s *surface) SetBufferScale(r *wayland.Surface, v int32) {
427437 if v < 1 {
@@ -43,9 +43,13 @@ type surface struct {
43 scale float6443 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.SurfaceContent46+ content ports.SurfaceContent
47- has bool47+ has bool
48- sub subState48+ 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 the53 // 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 *tearingHandler55 tearing *tearingHandler
@@ -200,6 +204,7 @@ func (s *surface) detach() {
200 }204 }
201 s.sub.parent = nil205 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 = false317 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, true367 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 = d432 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 {
7777 // token, until their window maps.
7878 terms map[string]*termSpawn
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
@@ -287,7 +288,7 @@ func New(cfg ports.Config, ch Channels) (*Core, error) {
287288 return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1")
288289 }
289290 // 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{}}
291292 if err := c.apply(cfg); err != nil {
292293 return nil, err
293294 }
@@ -564,6 +565,19 @@ func (c *Core) allLayers() []ports.LayerSurface {
564565 return all
565566 }
566567
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+
567581 // hit returns the surface under the global logical point and the point in
568582 // its surface coordinates: popups, then overlay and top layers, then
569583 // windows (fullscreen wins; otherwise the last visible placement is
@@ -580,7 +594,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
580594 if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
581595 return id, px, py
582596 }
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 {
584598 return id, px, py
585599 }
586600 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) {
597611 if full && !p.Floating {
598612 continue
599613 }
614+ if !c.acceptsInput(p.ID, lx-float64(r.X), ly-float64(r.Y)) {
615+ continue
616+ }
600617 if id == 0 || p.Fullscreen || p.Floating {
601618 id, sx, sy = p.ID, lx-float64(r.X), ly-float64(r.Y)
602619 }
@@ -604,7 +621,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
604621 }
605622 }
606623 if id == 0 {
607- return layerAt(sc, lx, ly, false)
624+ return c.layerAt(sc, lx, ly, false)
608625 }
609626 return id, sx, sy
610627 }
@@ -636,6 +653,8 @@ func (c *Core) Run(ctx context.Context) error {
636653 case ports.LayerChanged:
637654 c.layerChanged = true
638655 c.setLayers(v.Layers)
656+ case ports.InputRegionChanged:
657+ c.inputRegions[v.ID] = v
639658 case ports.WindowMapped:
640659 c.clients[v.ID] = v
641660 if v.Floating {
@@ -696,6 +715,7 @@ func (c *Core) Run(ctx context.Context) error {
696715 return nil
697716 }
698717 delete(c.clients, v.ID)
718+ delete(c.inputRegions, v.ID)
699719 delete(c.inhibitors, v.ID)
700720 delete(c.idle, v.ID)
701721 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]*termSpawn78 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.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
@@ -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, err293 return nil, err
293 }294 }
@@ -564,6 +565,19 @@ func (c *Core) allLayers() []ports.LayerSurface {
564 return all565 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 in581 // hit returns the surface under the global logical point and the point in
568 // its surface coordinates: popups, then overlay and top layers, then582 // its surface coordinates: popups, then overlay and top layers, then
569 // windows (fullscreen wins; otherwise the last visible placement is583 // 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, py595 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, py598 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 continue612 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, sy626 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 = true654 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] = v659 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 nil715 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) {
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+}
@@ -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 {
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