detached d2ed99b

fix(input): resync locked pointer, apply constraint regions after geometry and on entry

commit
d2ed99b9e63141aec33058876e6cd584dd63928d
author
brice · · 2026-09-25 12:47 UTC
parent
af33a35
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
9 files · +119 -24
M internal/adapters/libinput/libinput.go +9 -1
@@ -275,6 +275,9 @@ func translate(ev uintptr, opts Options, p *pointer) (ports.InputEvent, error) {
275275 // Absolute devices (tablets, VMs) map to the whole layout.
276276 pe := pointerEvent(ev)
277277 b := p.bounds()
278+ if p.constraint.Mode == ports.ConstraintLock {
279+ return ports.PointerMotion{X: p.x, Y: p.y, TimeMsec: now}, nil
280+ }
278281 x, y := p.set(float64(b.X)+pointerAbsX(pe, uint32(b.W)), float64(b.Y)+pointerAbsY(pe, uint32(b.H)))
279282 p.moved(opts.MoveCursor)
280283 return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil
@@ -379,8 +382,13 @@ func (p *pointer) set(x, y float64) (float64, float64) {
379382 return p.x, p.y
380383 }
381384
382-// constrain applies a new constraint; a confined pointer moves inside.
385+// constrain applies a new constraint. Entering or leaving a lock resyncs
386+// the pointer to core's cursor, which stood still meanwhile; a confined
387+// pointer moves inside.
383388 func (p *pointer) constrain(c ports.PointerConstraint) {
389+ if c.Mode == ports.ConstraintLock || p.constraint.Mode == ports.ConstraintLock {
390+ p.x, p.y = p.layout.Clamp(c.X, c.Y, c.X, c.Y)
391+ }
384392 p.constraint = c
385393 p.x, p.y = c.Clamp(p.x, p.y)
386394 }
@@ -275,6 +275,9 @@ func translate(ev uintptr, opts Options, p *pointer) (ports.InputEvent, error) {
275 // Absolute devices (tablets, VMs) map to the whole layout.275 // Absolute devices (tablets, VMs) map to the whole layout.
276 pe := pointerEvent(ev)276 pe := pointerEvent(ev)
277 b := p.bounds()277 b := p.bounds()
278+ if p.constraint.Mode == ports.ConstraintLock {
279+ return ports.PointerMotion{X: p.x, Y: p.y, TimeMsec: now}, nil
280+ }
278 x, y := p.set(float64(b.X)+pointerAbsX(pe, uint32(b.W)), float64(b.Y)+pointerAbsY(pe, uint32(b.H)))281 x, y := p.set(float64(b.X)+pointerAbsX(pe, uint32(b.W)), float64(b.Y)+pointerAbsY(pe, uint32(b.H)))
279 p.moved(opts.MoveCursor)282 p.moved(opts.MoveCursor)
280 return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil283 return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil
@@ -379,8 +382,13 @@ func (p *pointer) set(x, y float64) (float64, float64) {
379 return p.x, p.y382 return p.x, p.y
380 }383 }
381 384
382-// constrain applies a new constraint; a confined pointer moves inside.385+// constrain applies a new constraint. Entering or leaving a lock resyncs
386+// the pointer to core's cursor, which stood still meanwhile; a confined
387+// pointer moves inside.
383 func (p *pointer) constrain(c ports.PointerConstraint) {388 func (p *pointer) constrain(c ports.PointerConstraint) {
389+ if c.Mode == ports.ConstraintLock || p.constraint.Mode == ports.ConstraintLock {
390+ p.x, p.y = p.layout.Clamp(c.X, c.Y, c.X, c.Y)
391+ }
384 p.constraint = c392 p.constraint = c
385 p.x, p.y = c.Clamp(p.x, p.y)393 p.x, p.y = c.Clamp(p.x, p.y)
386 }394 }
M internal/adapters/libinput/libinput_test.go +12 -2
@@ -72,12 +72,12 @@ func TestPointerStartsOnPrimary(t *testing.T) {
7272
7373 func TestPointerConstraints(t *testing.T) {
7474 p := newPointer(ports.Layout{{Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 100, Scale: 1}})
75- p.constrain(ports.PointerConstraint{Mode: ports.ConstraintLock})
75+ p.constrain(ports.PointerConstraint{Mode: ports.ConstraintLock, X: 50, Y: 50})
7676 if m := p.move(10, -5); m.X != 50 || m.Y != 50 || m.DX != 10 || m.DY != -5 {
7777 t.Fatalf("locked: %+v", m)
7878 }
7979 // Confining moves the pointer inside, then keeps it there.
80- p.constrain(ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 60, Y: 10, W: 20, H: 20}})
80+ p.constrain(ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 60, Y: 10, W: 20, H: 20}, X: 50, Y: 50})
8181 if p.x != 60 || p.y != 29 {
8282 t.Fatal(p.x, p.y)
8383 }
@@ -88,4 +88,14 @@ func TestPointerConstraints(t *testing.T) {
8888 if m := p.move(10, 0); m.X != 89 {
8989 t.Fatalf("free: %+v", m)
9090 }
91+ // A lock takes core's cursor position, and so does its release.
92+ p.constrain(ports.PointerConstraint{Mode: ports.ConstraintLock, X: 20, Y: 30})
93+ if p.x != 20 || p.y != 30 {
94+ t.Fatal(p.x, p.y)
95+ }
96+ p.move(40, 40)
97+ p.constrain(ports.PointerConstraint{X: 21, Y: 31})
98+ if p.x != 21 || p.y != 31 {
99+ t.Fatal(p.x, p.y)
100+ }
91101 }
@@ -72,12 +72,12 @@ func TestPointerStartsOnPrimary(t *testing.T) {
72 72
73 func TestPointerConstraints(t *testing.T) {73 func TestPointerConstraints(t *testing.T) {
74 p := newPointer(ports.Layout{{Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 100, Scale: 1}})74 p := newPointer(ports.Layout{{Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 100, Scale: 1}})
75- p.constrain(ports.PointerConstraint{Mode: ports.ConstraintLock})75+ p.constrain(ports.PointerConstraint{Mode: ports.ConstraintLock, X: 50, Y: 50})
76 if m := p.move(10, -5); m.X != 50 || m.Y != 50 || m.DX != 10 || m.DY != -5 {76 if m := p.move(10, -5); m.X != 50 || m.Y != 50 || m.DX != 10 || m.DY != -5 {
77 t.Fatalf("locked: %+v", m)77 t.Fatalf("locked: %+v", m)
78 }78 }
79 // Confining moves the pointer inside, then keeps it there.79 // Confining moves the pointer inside, then keeps it there.
80- p.constrain(ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 60, Y: 10, W: 20, H: 20}})80+ p.constrain(ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 60, Y: 10, W: 20, H: 20}, X: 50, Y: 50})
81 if p.x != 60 || p.y != 29 {81 if p.x != 60 || p.y != 29 {
82 t.Fatal(p.x, p.y)82 t.Fatal(p.x, p.y)
83 }83 }
@@ -88,4 +88,14 @@ func TestPointerConstraints(t *testing.T) {
88 if m := p.move(10, 0); m.X != 89 {88 if m := p.move(10, 0); m.X != 89 {
89 t.Fatalf("free: %+v", m)89 t.Fatalf("free: %+v", m)
90 }90 }
91+ // A lock takes core's cursor position, and so does its release.
92+ p.constrain(ports.PointerConstraint{Mode: ports.ConstraintLock, X: 20, Y: 30})
93+ if p.x != 20 || p.y != 30 {
94+ t.Fatal(p.x, p.y)
95+ }
96+ p.move(40, 40)
97+ p.constrain(ports.PointerConstraint{X: 21, Y: 31})
98+ if p.x != 21 || p.y != 31 {
99+ t.Fatal(p.x, p.y)
100+ }
91 }101 }
M internal/adapters/wayland/constraints.go +40 -8
@@ -44,18 +44,22 @@ func (relativeHandler) Destroy(*relativepointer.ZwpRelativePointerV1) {}
4444
4545 // relativeMotion sends the motion deltas to the window client's relative
4646 // pointers. Absolute devices have no deltas and send nothing.
47-func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) {
47+// It reports whether any event was sent.
48+func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) bool {
4849 if c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 {
49- return
50+ return false
5051 }
5152 if !w.mapped || !w.xdg.resource.Resource.Alive() {
52- return
53+ return false
5354 }
55+ sent := false
5456 for _, rel := range s.relatives[w.xdg.resource.Client()] {
5557 if rel.Resource.Alive() {
5658 rel.SendRelativeMotion(uint32(c.TimeUsec>>32), uint32(c.TimeUsec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY))
59+ sent = true
5760 }
5861 }
62+ return sent
5963 }
6064
6165 // constraint is a zwp_locked_pointer_v1 or zwp_confined_pointer_v1 on a
@@ -77,6 +81,8 @@ func (constraintManager) Destroy(*pointerconstraints.ZwpPointerConstraintsV1) {}
7781 func (m constraintManager) LockPointer(r *pointerconstraints.ZwpPointerConstraintsV1, id uint32, surf *wayland.Surface, _ *wayland.Pointer, reg *wayland.Region, lifetime uint32) {
7882 c := m.create(r, surf, reg, lifetime)
7983 if c == nil {
84+ // An unknown surface: an inert object the client can destroy.
85+ _, _ = pointerconstraints.NewZwpLockedPointerV1(r.Client(), r.Version(), id, lockHandler{&constraint{}})
8086 return
8187 }
8288 lock, err := pointerconstraints.NewZwpLockedPointerV1(r.Client(), r.Version(), id, lockHandler{c})
@@ -91,6 +97,7 @@ func (m constraintManager) LockPointer(r *pointerconstraints.ZwpPointerConstrain
9197 func (m constraintManager) ConfinePointer(r *pointerconstraints.ZwpPointerConstraintsV1, id uint32, surf *wayland.Surface, _ *wayland.Pointer, reg *wayland.Region, lifetime uint32) {
9298 c := m.create(r, surf, reg, lifetime)
9399 if c == nil {
100+ _, _ = pointerconstraints.NewZwpConfinedPointerV1(r.Client(), r.Version(), id, confineHandler{&constraint{}})
94101 return
95102 }
96103 confine, err := pointerconstraints.NewZwpConfinedPointerV1(r.Client(), r.Version(), id, confineHandler{c})
@@ -117,6 +124,9 @@ func (m constraintManager) create(r *pointerconstraints.ZwpPointerConstraintsV1,
117124 r.PostError(uint32(pointerconstraints.ZwpPointerConstraintsV1ErrorAlreadyConstrained), "surface already constrained")
118125 return nil
119126 }
127+ if state.kind == roleCursor {
128+ return nil
129+ }
120130 c := &constraint{surface: state, region: s.regionBox(reg), persistent: lifetime == uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)}
121131 s.constraints[state] = c
122132 return c
@@ -141,6 +151,9 @@ func (lockHandler) Destroy(*pointerconstraints.ZwpLockedPointerV1) {}
141151 func (lockHandler) SetCursorPositionHint(*pointerconstraints.ZwpLockedPointerV1, server.Fixed, server.Fixed) {
142152 }
143153 func (h lockHandler) SetRegion(_ *pointerconstraints.ZwpLockedPointerV1, reg *wayland.Region) {
154+ if h.c.surface == nil {
155+ return
156+ }
144157 r := h.c.surface.server.regionBox(reg)
145158 h.c.pending = &r
146159 }
@@ -149,19 +162,27 @@ type confineHandler struct{ c *constraint }
149162
150163 func (confineHandler) Destroy(*pointerconstraints.ZwpConfinedPointerV1) {}
151164 func (h confineHandler) SetRegion(_ *pointerconstraints.ZwpConfinedPointerV1, reg *wayland.Region) {
165+ if h.c.surface == nil {
166+ return
167+ }
152168 r := h.c.surface.server.regionBox(reg)
153169 h.c.pending = &r
154170 }
155171
156-// commitConstraint applies a pending region on the surface commit.
157-func (s *surface) commitConstraint() {
172+// commitConstraint applies a pending region on the surface commit, after
173+// the new window geometry: core receives the region window-local.
174+func (s *surface) commitConstraint(geometry bool) {
158175 c := s.server.constraints[s]
159- if c == nil || c.pending == nil {
176+ if c == nil || (c.pending == nil && !geometry) {
160177 return
161178 }
162- c.region, c.pending = *c.pending, nil
179+ if c.pending != nil {
180+ c.region, c.pending = *c.pending, nil
181+ }
163182 if c.active {
164183 s.server.emitConstraint(c)
184+ } else {
185+ s.server.updateConstraint()
165186 }
166187 }
167188
@@ -189,7 +210,7 @@ func (s *Server) dropConstraint(c *constraint) {
189210 func (s *Server) updateConstraint() {
190211 var want *constraint
191212 if w := s.windows[s.pointerFocus]; w != nil && w.mapped && s.pointerFocus == s.focused {
192- if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct {
213+ if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct && (c.active || c.contains(w)) {
193214 want = c
194215 }
195216 }
@@ -240,6 +261,17 @@ func (s *Server) emitConstraint(c *constraint) {
240261 s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}})
241262 }
242263
264+// contains reports whether the pointer is in the region: a constraint
265+// activates only there, so it never warps the pointer.
266+func (c *constraint) contains(w *window) bool {
267+ r := c.region
268+ if r.W <= 0 || r.H <= 0 {
269+ return true
270+ }
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)
273+}
274+
243275 // locked reports whether the window's pointer is locked.
244276 func (s *Server) locked(w *window) bool {
245277 return s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.xdg.surface
@@ -44,18 +44,22 @@ func (relativeHandler) Destroy(*relativepointer.ZwpRelativePointerV1) {}
44 44
45 // relativeMotion sends the motion deltas to the window client's relative45 // relativeMotion sends the motion deltas to the window client's relative
46 // pointers. Absolute devices have no deltas and send nothing.46 // pointers. Absolute devices have no deltas and send nothing.
47-func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) {47+// It reports whether any event was sent.
48+func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) bool {
48 if c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 {49 if c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 {
49- return50+ return false
50 }51 }
51 if !w.mapped || !w.xdg.resource.Resource.Alive() {52 if !w.mapped || !w.xdg.resource.Resource.Alive() {
52- return53+ return false
53 }54 }
55+ sent := false
54 for _, rel := range s.relatives[w.xdg.resource.Client()] {56 for _, rel := range s.relatives[w.xdg.resource.Client()] {
55 if rel.Resource.Alive() {57 if rel.Resource.Alive() {
56 rel.SendRelativeMotion(uint32(c.TimeUsec>>32), uint32(c.TimeUsec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY))58 rel.SendRelativeMotion(uint32(c.TimeUsec>>32), uint32(c.TimeUsec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY))
59+ sent = true
57 }60 }
58 }61 }
62+ return sent
59 }63 }
60 64
61 // constraint is a zwp_locked_pointer_v1 or zwp_confined_pointer_v1 on a65 // constraint is a zwp_locked_pointer_v1 or zwp_confined_pointer_v1 on a
@@ -77,6 +81,8 @@ func (constraintManager) Destroy(*pointerconstraints.ZwpPointerConstraintsV1) {}
77 func (m constraintManager) LockPointer(r *pointerconstraints.ZwpPointerConstraintsV1, id uint32, surf *wayland.Surface, _ *wayland.Pointer, reg *wayland.Region, lifetime uint32) {81 func (m constraintManager) LockPointer(r *pointerconstraints.ZwpPointerConstraintsV1, id uint32, surf *wayland.Surface, _ *wayland.Pointer, reg *wayland.Region, lifetime uint32) {
78 c := m.create(r, surf, reg, lifetime)82 c := m.create(r, surf, reg, lifetime)
79 if c == nil {83 if c == nil {
84+ // An unknown surface: an inert object the client can destroy.
85+ _, _ = pointerconstraints.NewZwpLockedPointerV1(r.Client(), r.Version(), id, lockHandler{&constraint{}})
80 return86 return
81 }87 }
82 lock, err := pointerconstraints.NewZwpLockedPointerV1(r.Client(), r.Version(), id, lockHandler{c})88 lock, err := pointerconstraints.NewZwpLockedPointerV1(r.Client(), r.Version(), id, lockHandler{c})
@@ -91,6 +97,7 @@ func (m constraintManager) LockPointer(r *pointerconstraints.ZwpPointerConstrain
91 func (m constraintManager) ConfinePointer(r *pointerconstraints.ZwpPointerConstraintsV1, id uint32, surf *wayland.Surface, _ *wayland.Pointer, reg *wayland.Region, lifetime uint32) {97 func (m constraintManager) ConfinePointer(r *pointerconstraints.ZwpPointerConstraintsV1, id uint32, surf *wayland.Surface, _ *wayland.Pointer, reg *wayland.Region, lifetime uint32) {
92 c := m.create(r, surf, reg, lifetime)98 c := m.create(r, surf, reg, lifetime)
93 if c == nil {99 if c == nil {
100+ _, _ = pointerconstraints.NewZwpConfinedPointerV1(r.Client(), r.Version(), id, confineHandler{&constraint{}})
94 return101 return
95 }102 }
96 confine, err := pointerconstraints.NewZwpConfinedPointerV1(r.Client(), r.Version(), id, confineHandler{c})103 confine, err := pointerconstraints.NewZwpConfinedPointerV1(r.Client(), r.Version(), id, confineHandler{c})
@@ -117,6 +124,9 @@ func (m constraintManager) create(r *pointerconstraints.ZwpPointerConstraintsV1,
117 r.PostError(uint32(pointerconstraints.ZwpPointerConstraintsV1ErrorAlreadyConstrained), "surface already constrained")124 r.PostError(uint32(pointerconstraints.ZwpPointerConstraintsV1ErrorAlreadyConstrained), "surface already constrained")
118 return nil125 return nil
119 }126 }
127+ if state.kind == roleCursor {
128+ return nil
129+ }
120 c := &constraint{surface: state, region: s.regionBox(reg), persistent: lifetime == uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)}130 c := &constraint{surface: state, region: s.regionBox(reg), persistent: lifetime == uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)}
121 s.constraints[state] = c131 s.constraints[state] = c
122 return c132 return c
@@ -141,6 +151,9 @@ func (lockHandler) Destroy(*pointerconstraints.ZwpLockedPointerV1) {}
141 func (lockHandler) SetCursorPositionHint(*pointerconstraints.ZwpLockedPointerV1, server.Fixed, server.Fixed) {151 func (lockHandler) SetCursorPositionHint(*pointerconstraints.ZwpLockedPointerV1, server.Fixed, server.Fixed) {
142 }152 }
143 func (h lockHandler) SetRegion(_ *pointerconstraints.ZwpLockedPointerV1, reg *wayland.Region) {153 func (h lockHandler) SetRegion(_ *pointerconstraints.ZwpLockedPointerV1, reg *wayland.Region) {
154+ if h.c.surface == nil {
155+ return
156+ }
144 r := h.c.surface.server.regionBox(reg)157 r := h.c.surface.server.regionBox(reg)
145 h.c.pending = &r158 h.c.pending = &r
146 }159 }
@@ -149,19 +162,27 @@ type confineHandler struct{ c *constraint }
149 162
150 func (confineHandler) Destroy(*pointerconstraints.ZwpConfinedPointerV1) {}163 func (confineHandler) Destroy(*pointerconstraints.ZwpConfinedPointerV1) {}
151 func (h confineHandler) SetRegion(_ *pointerconstraints.ZwpConfinedPointerV1, reg *wayland.Region) {164 func (h confineHandler) SetRegion(_ *pointerconstraints.ZwpConfinedPointerV1, reg *wayland.Region) {
165+ if h.c.surface == nil {
166+ return
167+ }
152 r := h.c.surface.server.regionBox(reg)168 r := h.c.surface.server.regionBox(reg)
153 h.c.pending = &r169 h.c.pending = &r
154 }170 }
155 171
156-// commitConstraint applies a pending region on the surface commit.172+// commitConstraint applies a pending region on the surface commit, after
157-func (s *surface) commitConstraint() {173+// the new window geometry: core receives the region window-local.
174+func (s *surface) commitConstraint(geometry bool) {
158 c := s.server.constraints[s]175 c := s.server.constraints[s]
159- if c == nil || c.pending == nil {176+ if c == nil || (c.pending == nil && !geometry) {
160 return177 return
161 }178 }
162- c.region, c.pending = *c.pending, nil179+ if c.pending != nil {
180+ c.region, c.pending = *c.pending, nil
181+ }
163 if c.active {182 if c.active {
164 s.server.emitConstraint(c)183 s.server.emitConstraint(c)
184+ } else {
185+ s.server.updateConstraint()
165 }186 }
166 }187 }
167 188
@@ -189,7 +210,7 @@ func (s *Server) dropConstraint(c *constraint) {
189 func (s *Server) updateConstraint() {210 func (s *Server) updateConstraint() {
190 var want *constraint211 var want *constraint
191 if w := s.windows[s.pointerFocus]; w != nil && w.mapped && s.pointerFocus == s.focused {212 if w := s.windows[s.pointerFocus]; w != nil && w.mapped && s.pointerFocus == s.focused {
192- if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct {213+ if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct && (c.active || c.contains(w)) {
193 want = c214 want = c
194 }215 }
195 }216 }
@@ -240,6 +261,17 @@ func (s *Server) emitConstraint(c *constraint) {
240 s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}})261 s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}})
241 }262 }
242 263
264+// contains reports whether the pointer is in the region: a constraint
265+// activates only there, so it never warps the pointer.
266+func (c *constraint) contains(w *window) bool {
267+ r := c.region
268+ if r.W <= 0 || r.H <= 0 {
269+ return true
270+ }
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)
273+}
274+
243 // locked reports whether the window's pointer is locked.275 // locked reports whether the window's pointer is locked.
244 func (s *Server) locked(w *window) bool {276 func (s *Server) locked(w *window) bool {
245 return s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.xdg.surface277 return s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.xdg.surface
M internal/adapters/wayland/constraints_test.go +35 -1
@@ -151,8 +151,16 @@ func TestPointerConfineOneshotRegion(t *testing.T) {
151151 requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, region, uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot))
152152 // The region is copied: changing it later has no effect.
153153 requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(100), int32(100))
154+ // Outside the region the constraint waits for the pointer.
154155 commands <- ports.FocusWindow{ID: w.ID}
155156 commands <- ports.PointerFocus{ID: w.ID}
157+ if err := c.Roundtrip(); err != nil {
158+ t.Fatal(err)
159+ }
160+ if len(confineEvents) > 0 {
161+ t.Fatal("confined outside the region")
162+ }
163+ commands <- ports.PointerMotionTo{ID: w.ID, X: 5, Y: 5}
156164 if ev := next(t, c, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventConfined) {
157165 t.Fatalf("confine event %v", ev)
158166 }
@@ -173,7 +181,7 @@ func TestPointerConfineOneshotRegion(t *testing.T) {
173181 if ev := next(t, c, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventUnconfined) {
174182 t.Fatalf("unconfine event %v", ev)
175183 }
176- commands <- ports.PointerFocus{ID: w.ID}
184+ commands <- ports.PointerFocus{ID: w.ID, X: 5, Y: 5}
177185 if err := c.Roundtrip(); err != nil {
178186 t.Fatal(err)
179187 }
@@ -189,3 +197,29 @@ func TestPointerConfineOneshotRegion(t *testing.T) {
189197 requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, again, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot))
190198 expectProtocolError(t, c, constraints, uint32(pointerconstraints.ZwpPointerConstraintsV1ErrorAlreadyConstrained))
191199 }
200+
201+// Destroying an active lock unlocks the pointer in core.
202+func TestPointerLockDestroyed(t *testing.T) {
203+ s, events, commands, dir := lifecycleServer(t)
204+ c := protocolClient(t, s, dir)
205+ seat := bindProtocol(t, c, "wl_seat")
206+ registerProtocol(t, c, seat)
207+ pointer := c.AllocateID()
208+ registerProtocol(t, c, pointer)
209+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
210+ w, surf := surfaceMapper(t, c, events)()
211+ constraints := bindProtocol(t, c, "zwp_pointer_constraints_v1")
212+ lock := c.AllocateID()
213+ lockEvents := logProxy(t, c, lock)
214+ requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent))
215+ commands <- ports.PointerFocus{ID: w.ID}
216+ commands <- ports.FocusWindow{ID: w.ID}
217+ next(t, c, lockEvents)
218+ if got := constrained(t, events); got.Mode != ports.ConstraintLock {
219+ t.Fatalf("constrained %+v", got)
220+ }
221+ requestProtocol(t, c, lock, pointerconstraints.ZwpLockedPointerV1RequestDestroy)
222+ if got := constrained(t, events); got.ID != 0 {
223+ t.Fatalf("after destroy %+v", got)
224+ }
225+}
@@ -151,8 +151,16 @@ func TestPointerConfineOneshotRegion(t *testing.T) {
151 requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, region, uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot))151 requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, region, uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot))
152 // The region is copied: changing it later has no effect.152 // The region is copied: changing it later has no effect.
153 requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(100), int32(100))153 requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(100), int32(100))
154+ // Outside the region the constraint waits for the pointer.
154 commands <- ports.FocusWindow{ID: w.ID}155 commands <- ports.FocusWindow{ID: w.ID}
155 commands <- ports.PointerFocus{ID: w.ID}156 commands <- ports.PointerFocus{ID: w.ID}
157+ if err := c.Roundtrip(); err != nil {
158+ t.Fatal(err)
159+ }
160+ if len(confineEvents) > 0 {
161+ t.Fatal("confined outside the region")
162+ }
163+ commands <- ports.PointerMotionTo{ID: w.ID, X: 5, Y: 5}
156 if ev := next(t, c, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventConfined) {164 if ev := next(t, c, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventConfined) {
157 t.Fatalf("confine event %v", ev)165 t.Fatalf("confine event %v", ev)
158 }166 }
@@ -173,7 +181,7 @@ func TestPointerConfineOneshotRegion(t *testing.T) {
173 if ev := next(t, c, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventUnconfined) {181 if ev := next(t, c, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventUnconfined) {
174 t.Fatalf("unconfine event %v", ev)182 t.Fatalf("unconfine event %v", ev)
175 }183 }
176- commands <- ports.PointerFocus{ID: w.ID}184+ commands <- ports.PointerFocus{ID: w.ID, X: 5, Y: 5}
177 if err := c.Roundtrip(); err != nil {185 if err := c.Roundtrip(); err != nil {
178 t.Fatal(err)186 t.Fatal(err)
179 }187 }
@@ -189,3 +197,29 @@ func TestPointerConfineOneshotRegion(t *testing.T) {
189 requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, again, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot))197 requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, again, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot))
190 expectProtocolError(t, c, constraints, uint32(pointerconstraints.ZwpPointerConstraintsV1ErrorAlreadyConstrained))198 expectProtocolError(t, c, constraints, uint32(pointerconstraints.ZwpPointerConstraintsV1ErrorAlreadyConstrained))
191 }199 }
200+
201+// Destroying an active lock unlocks the pointer in core.
202+func TestPointerLockDestroyed(t *testing.T) {
203+ s, events, commands, dir := lifecycleServer(t)
204+ c := protocolClient(t, s, dir)
205+ seat := bindProtocol(t, c, "wl_seat")
206+ registerProtocol(t, c, seat)
207+ pointer := c.AllocateID()
208+ registerProtocol(t, c, pointer)
209+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
210+ w, surf := surfaceMapper(t, c, events)()
211+ constraints := bindProtocol(t, c, "zwp_pointer_constraints_v1")
212+ lock := c.AllocateID()
213+ lockEvents := logProxy(t, c, lock)
214+ requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent))
215+ commands <- ports.PointerFocus{ID: w.ID}
216+ commands <- ports.FocusWindow{ID: w.ID}
217+ next(t, c, lockEvents)
218+ if got := constrained(t, events); got.Mode != ports.ConstraintLock {
219+ t.Fatalf("constrained %+v", got)
220+ }
221+ requestProtocol(t, c, lock, pointerconstraints.ZwpLockedPointerV1RequestDestroy)
222+ if got := constrained(t, events); got.ID != 0 {
223+ t.Fatalf("after destroy %+v", got)
224+ }
225+}
M internal/adapters/wayland/server.go +8 -3
@@ -367,16 +367,21 @@ func (s *Server) apply(cmd ports.ClientCommand) {
367367 s.changePointerFocus(c.ID, c.X, c.Y)
368368 case ports.PointerMotionTo:
369369 if w := s.windows[c.ID]; w != nil && c.ID == s.pointerFocus {
370- s.relativeMotion(w, c)
371- if s.locked(w) {
372- // Only relative motion reaches a locked pointer.
370+ if s.relativeMotion(w, c) && s.locked(w) {
373371 for _, p := range s.windowPointers(w) {
374372 pointerFrame(p)
375373 }
374+ }
375+ if s.locked(w) {
376+ // Only relative motion reaches a locked pointer.
376377 return
377378 }
378379 x, y := w.surfacePoint(c.X, c.Y)
379380 s.pointerX, s.pointerY = c.X, c.Y
381+ // A constraint waits for the pointer to enter its region.
382+ if s.constraint == nil {
383+ s.updateConstraint()
384+ }
380385 for _, p := range s.windowPointers(w) {
381386 p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y))
382387 pointerFrame(p)
@@ -367,16 +367,21 @@ func (s *Server) apply(cmd ports.ClientCommand) {
367 s.changePointerFocus(c.ID, c.X, c.Y)367 s.changePointerFocus(c.ID, c.X, c.Y)
368 case ports.PointerMotionTo:368 case ports.PointerMotionTo:
369 if w := s.windows[c.ID]; w != nil && c.ID == s.pointerFocus {369 if w := s.windows[c.ID]; w != nil && c.ID == s.pointerFocus {
370- s.relativeMotion(w, c)370+ if s.relativeMotion(w, c) && s.locked(w) {
371- if s.locked(w) {
372- // Only relative motion reaches a locked pointer.
373 for _, p := range s.windowPointers(w) {371 for _, p := range s.windowPointers(w) {
374 pointerFrame(p)372 pointerFrame(p)
375 }373 }
374+ }
375+ if s.locked(w) {
376+ // Only relative motion reaches a locked pointer.
376 return377 return
377 }378 }
378 x, y := w.surfacePoint(c.X, c.Y)379 x, y := w.surfacePoint(c.X, c.Y)
379 s.pointerX, s.pointerY = c.X, c.Y380 s.pointerX, s.pointerY = c.X, c.Y
381+ // A constraint waits for the pointer to enter its region.
382+ if s.constraint == nil {
383+ s.updateConstraint()
384+ }
380 for _, p := range s.windowPointers(w) {385 for _, p := range s.windowPointers(w) {
381 p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y))386 p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y))
382 pointerFrame(p)387 pointerFrame(p)
M internal/adapters/wayland/surface.go +1 -1
@@ -197,7 +197,6 @@ func (s *surface) Commit(*wayland.Surface) {
197197 s.pending = nil
198198 s.attached = false
199199 }
200- s.commitConstraint()
201200 s.server.awaiting = append(s.server.awaiting, s.callbacks...)
202201 s.callbacks = nil
203202 if s.role != nil {
@@ -234,6 +233,7 @@ func (s *surface) Commit(*wayland.Surface) {
234233 if s.xdg != nil && s.xdg.pendingGeometry != s.xdg.geometry {
235234 s.xdg.geometry, geometry = s.xdg.pendingGeometry, true
236235 }
236+ s.commitConstraint(geometry)
237237 if fresh || moved || geometry || s.sub.parent != nil {
238238 s.redraw()
239239 }
@@ -197,7 +197,6 @@ func (s *surface) Commit(*wayland.Surface) {
197 s.pending = nil197 s.pending = nil
198 s.attached = false198 s.attached = false
199 }199 }
200- s.commitConstraint()
201 s.server.awaiting = append(s.server.awaiting, s.callbacks...)200 s.server.awaiting = append(s.server.awaiting, s.callbacks...)
202 s.callbacks = nil201 s.callbacks = nil
203 if s.role != nil {202 if s.role != nil {
@@ -234,6 +233,7 @@ func (s *surface) Commit(*wayland.Surface) {
234 if s.xdg != nil && s.xdg.pendingGeometry != s.xdg.geometry {233 if s.xdg != nil && s.xdg.pendingGeometry != s.xdg.geometry {
235 s.xdg.geometry, geometry = s.xdg.pendingGeometry, true234 s.xdg.geometry, geometry = s.xdg.pendingGeometry, true
236 }235 }
236+ s.commitConstraint(geometry)
237 if fresh || moved || geometry || s.sub.parent != nil {237 if fresh || moved || geometry || s.sub.parent != nil {
238 s.redraw()238 s.redraw()
239 }239 }
M internal/core/core.go +5 -2
@@ -444,11 +444,14 @@ func (c *Core) resolveConstraint() {
444444 }
445445 }
446446 }
447- if g == c.constraint {
447+ // The cursor follows core; input resyncs to it only on a change.
448+ if g.Mode == c.constraint.Mode && g.Rect == c.constraint.Rect {
448449 return
449450 }
450- c.constraint = g
451+ // A layout change must not leave a locked cursor off its window.
451452 c.cursorX, c.cursorY = g.Clamp(c.cursorX, c.cursorY)
453+ g.X, g.Y = c.cursorX, c.cursorY
454+ c.constraint = g
452455 if c.ch.Constraints != nil {
453456 latest(c.ch.Constraints, g)
454457 }
@@ -444,11 +444,14 @@ func (c *Core) resolveConstraint() {
444 }444 }
445 }445 }
446 }446 }
447- if g == c.constraint {447+ // The cursor follows core; input resyncs to it only on a change.
448+ if g.Mode == c.constraint.Mode && g.Rect == c.constraint.Rect {
448 return449 return
449 }450 }
450- c.constraint = g451+ // A layout change must not leave a locked cursor off its window.
451 c.cursorX, c.cursorY = g.Clamp(c.cursorX, c.cursorY)452 c.cursorX, c.cursorY = g.Clamp(c.cursorX, c.cursorY)
453+ g.X, g.Y = c.cursorX, c.cursorY
454+ c.constraint = g
452 if c.ch.Constraints != nil {455 if c.ch.Constraints != nil {
453 latest(c.ch.Constraints, g)456 latest(c.ch.Constraints, g)
454 }457 }
M internal/core/core_test.go +3 -2
@@ -600,7 +600,8 @@ func TestPointerConstraint(t *testing.T) {
600600 client <- ports.PointerConstrained{ID: 1, PointerConstraint: ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 10, Y: 10, W: 1000, H: 20}}}
601601 scene(t, scenes)
602602 want := ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 100 + r.X + 10, Y: r.Y + 10, W: r.W - 10, H: 20}}
603- if got := receive(t, constraints); got != want {
603+ // Input gets core's cursor, clamped into the region.
604+ if got := receive(t, constraints); got.Mode != want.Mode || got.Rect != want.Rect || got.X != 150 || got.Y != 29 {
604605 t.Fatalf("got %+v, want %+v", got, want)
605606 }
606607 for len(commands) > 0 {
@@ -636,7 +637,7 @@ func TestPointerConstraint(t *testing.T) {
636637 }
637638 client <- ports.PointerConstrained{}
638639 scene(t, scenes)
639- if got := receive(t, constraints); got != (ports.PointerConstraint{}) {
640+ if got := receive(t, constraints); got.Mode != ports.ConstraintNone {
640641 t.Fatalf("release %+v", got)
641642 }
642643 }
@@ -600,7 +600,8 @@ func TestPointerConstraint(t *testing.T) {
600 client <- ports.PointerConstrained{ID: 1, PointerConstraint: ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 10, Y: 10, W: 1000, H: 20}}}600 client <- ports.PointerConstrained{ID: 1, PointerConstraint: ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 10, Y: 10, W: 1000, H: 20}}}
601 scene(t, scenes)601 scene(t, scenes)
602 want := ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 100 + r.X + 10, Y: r.Y + 10, W: r.W - 10, H: 20}}602 want := ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 100 + r.X + 10, Y: r.Y + 10, W: r.W - 10, H: 20}}
603- if got := receive(t, constraints); got != want {603+ // Input gets core's cursor, clamped into the region.
604+ if got := receive(t, constraints); got.Mode != want.Mode || got.Rect != want.Rect || got.X != 150 || got.Y != 29 {
604 t.Fatalf("got %+v, want %+v", got, want)605 t.Fatalf("got %+v, want %+v", got, want)
605 }606 }
606 for len(commands) > 0 {607 for len(commands) > 0 {
@@ -636,7 +637,7 @@ func TestPointerConstraint(t *testing.T) {
636 }637 }
637 client <- ports.PointerConstrained{}638 client <- ports.PointerConstrained{}
638 scene(t, scenes)639 scene(t, scenes)
639- if got := receive(t, constraints); got != (ports.PointerConstraint{}) {640+ if got := receive(t, constraints); got.Mode != ports.ConstraintNone {
640 t.Fatalf("release %+v", got)641 t.Fatalf("release %+v", got)
641 }642 }
642 }643 }
M internal/ports/messages.go +6 -4
@@ -96,16 +96,18 @@ const (
9696
9797 // PointerConstraint is an active pointer constraint. Rect is logical: in
9898 // PointerConstrained it is window-local and empty means the whole window;
99-// core sends input the resolved global rectangle.
99+// core sends input the resolved global rectangle, and in X, Y its cursor
100+// position, where input holds a locked pointer.
100101 type PointerConstraint struct {
101102 Mode ConstraintMode
102103 Rect Rect
104+ X, Y float64
103105 }
104106
105-// Clamp keeps a global point inside a confine rectangle; other modes and an
106-// empty rectangle leave it unchanged.
107+// Clamp keeps a global point inside the rectangle of a lock or confine;
108+// no constraint and an empty rectangle leave it unchanged.
107109 func (c PointerConstraint) Clamp(x, y float64) (float64, float64) {
108- if c.Mode != ConstraintConfine || c.Rect.W <= 0 || c.Rect.H <= 0 {
110+ if c.Mode == ConstraintNone || c.Rect.W <= 0 || c.Rect.H <= 0 {
109111 return x, y
110112 }
111113 x = min(max(x, float64(c.Rect.X)), float64(c.Rect.X+c.Rect.W-1))
@@ -96,16 +96,18 @@ const (
96 96
97 // PointerConstraint is an active pointer constraint. Rect is logical: in97 // PointerConstraint is an active pointer constraint. Rect is logical: in
98 // PointerConstrained it is window-local and empty means the whole window;98 // PointerConstrained it is window-local and empty means the whole window;
99-// core sends input the resolved global rectangle.99+// core sends input the resolved global rectangle, and in X, Y its cursor
100+// position, where input holds a locked pointer.
100 type PointerConstraint struct {101 type PointerConstraint struct {
101 Mode ConstraintMode102 Mode ConstraintMode
102 Rect Rect103 Rect Rect
104+ X, Y float64
103 }105 }
104 106
105-// Clamp keeps a global point inside a confine rectangle; other modes and an107+// Clamp keeps a global point inside the rectangle of a lock or confine;
106-// empty rectangle leave it unchanged.108+// no constraint and an empty rectangle leave it unchanged.
107 func (c PointerConstraint) Clamp(x, y float64) (float64, float64) {109 func (c PointerConstraint) Clamp(x, y float64) (float64, float64) {
108- if c.Mode != ConstraintConfine || c.Rect.W <= 0 || c.Rect.H <= 0 {110+ if c.Mode == ConstraintNone || c.Rect.W <= 0 || c.Rect.H <= 0 {
109 return x, y111 return x, y
110 }112 }
111 x = min(max(x, float64(c.Rect.X)), float64(c.Rect.X+c.Rect.W-1))113 x = min(max(x, float64(c.Rect.X)), float64(c.Rect.X+c.Rect.W-1))

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 26

Tags 10

↑↓ moveenter openesc close