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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 275 | 275 | // Absolute devices (tablets, VMs) map to the whole layout. | |
| 276 | 276 | pe := pointerEvent(ev) | |
| 277 | 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 | 281 | x, y := p.set(float64(b.X)+pointerAbsX(pe, uint32(b.W)), float64(b.Y)+pointerAbsY(pe, uint32(b.H))) | |
| 279 | 282 | p.moved(opts.MoveCursor) | |
| 280 | 283 | return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil | |
| @@ -379,8 +382,13 @@ func (p *pointer) set(x, y float64) (float64, float64) { | |||
| 379 | 382 | 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 | 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 | 392 | p.constraint = c | |
| 385 | 393 | p.x, p.y = c.Clamp(p.x, p.y) | |
| 386 | 394 | } | |
| @@ -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}, nil | 283 | 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.y | 382 | 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 = c | 392 | 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) { | |||
| 72 | 72 | ||
| 73 | 73 | func TestPointerConstraints(t *testing.T) { | |
| 74 | 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 | 76 | if m := p.move(10, -5); m.X != 50 || m.Y != 50 || m.DX != 10 || m.DY != -5 { | |
| 77 | 77 | t.Fatalf("locked: %+v", m) | |
| 78 | 78 | } | |
| 79 | 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 | 81 | if p.x != 60 || p.y != 29 { | |
| 82 | 82 | t.Fatal(p.x, p.y) | |
| 83 | 83 | } | |
| @@ -88,4 +88,14 @@ func TestPointerConstraints(t *testing.T) { | |||
| 88 | 88 | if m := p.move(10, 0); m.X != 89 { | |
| 89 | 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 | } | |
| @@ -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) {} | |||
| 44 | 44 | ||
| 45 | 45 | // relativeMotion sends the motion deltas to the window client's relative | |
| 46 | 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 | 49 | if c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 { | |
| 49 | - | return | |
| 50 | + | return false | |
| 50 | 51 | } | |
| 51 | 52 | if !w.mapped || !w.xdg.resource.Resource.Alive() { | |
| 52 | - | return | |
| 53 | + | return false | |
| 53 | 54 | } | |
| 55 | + | sent := false | |
| 54 | 56 | for _, rel := range s.relatives[w.xdg.resource.Client()] { | |
| 55 | 57 | if rel.Resource.Alive() { | |
| 56 | 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 | 65 | // constraint is a zwp_locked_pointer_v1 or zwp_confined_pointer_v1 on a | |
| @@ -77,6 +81,8 @@ func (constraintManager) Destroy(*pointerconstraints.ZwpPointerConstraintsV1) {} | |||
| 77 | 81 | func (m constraintManager) LockPointer(r *pointerconstraints.ZwpPointerConstraintsV1, id uint32, surf *wayland.Surface, _ *wayland.Pointer, reg *wayland.Region, lifetime uint32) { | |
| 78 | 82 | c := m.create(r, surf, reg, lifetime) | |
| 79 | 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 | 86 | return | |
| 81 | 87 | } | |
| 82 | 88 | lock, err := pointerconstraints.NewZwpLockedPointerV1(r.Client(), r.Version(), id, lockHandler{c}) | |
| @@ -91,6 +97,7 @@ func (m constraintManager) LockPointer(r *pointerconstraints.ZwpPointerConstrain | |||
| 91 | 97 | func (m constraintManager) ConfinePointer(r *pointerconstraints.ZwpPointerConstraintsV1, id uint32, surf *wayland.Surface, _ *wayland.Pointer, reg *wayland.Region, lifetime uint32) { | |
| 92 | 98 | c := m.create(r, surf, reg, lifetime) | |
| 93 | 99 | if c == nil { | |
| 100 | + | _, _ = pointerconstraints.NewZwpConfinedPointerV1(r.Client(), r.Version(), id, confineHandler{&constraint{}}) | |
| 94 | 101 | return | |
| 95 | 102 | } | |
| 96 | 103 | confine, err := pointerconstraints.NewZwpConfinedPointerV1(r.Client(), r.Version(), id, confineHandler{c}) | |
| @@ -117,6 +124,9 @@ func (m constraintManager) create(r *pointerconstraints.ZwpPointerConstraintsV1, | |||
| 117 | 124 | r.PostError(uint32(pointerconstraints.ZwpPointerConstraintsV1ErrorAlreadyConstrained), "surface already constrained") | |
| 118 | 125 | return nil | |
| 119 | 126 | } | |
| 127 | + | if state.kind == roleCursor { | |
| 128 | + | return nil | |
| 129 | + | } | |
| 120 | 130 | c := &constraint{surface: state, region: s.regionBox(reg), persistent: lifetime == uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)} | |
| 121 | 131 | s.constraints[state] = c | |
| 122 | 132 | return c | |
| @@ -141,6 +151,9 @@ func (lockHandler) Destroy(*pointerconstraints.ZwpLockedPointerV1) {} | |||
| 141 | 151 | func (lockHandler) SetCursorPositionHint(*pointerconstraints.ZwpLockedPointerV1, server.Fixed, server.Fixed) { | |
| 142 | 152 | } | |
| 143 | 153 | func (h lockHandler) SetRegion(_ *pointerconstraints.ZwpLockedPointerV1, reg *wayland.Region) { | |
| 154 | + | if h.c.surface == nil { | |
| 155 | + | return | |
| 156 | + | } | |
| 144 | 157 | r := h.c.surface.server.regionBox(reg) | |
| 145 | 158 | h.c.pending = &r | |
| 146 | 159 | } | |
| @@ -149,19 +162,27 @@ type confineHandler struct{ c *constraint } | |||
| 149 | 162 | ||
| 150 | 163 | func (confineHandler) Destroy(*pointerconstraints.ZwpConfinedPointerV1) {} | |
| 151 | 164 | func (h confineHandler) SetRegion(_ *pointerconstraints.ZwpConfinedPointerV1, reg *wayland.Region) { | |
| 165 | + | if h.c.surface == nil { | |
| 166 | + | return | |
| 167 | + | } | |
| 152 | 168 | r := h.c.surface.server.regionBox(reg) | |
| 153 | 169 | h.c.pending = &r | |
| 154 | 170 | } | |
| 155 | 171 | ||
| 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) { | |
| 158 | 175 | c := s.server.constraints[s] | |
| 159 | - | if c == nil || c.pending == nil { | |
| 176 | + | if c == nil || (c.pending == nil && !geometry) { | |
| 160 | 177 | return | |
| 161 | 178 | } | |
| 162 | - | c.region, c.pending = *c.pending, nil | |
| 179 | + | if c.pending != nil { | |
| 180 | + | c.region, c.pending = *c.pending, nil | |
| 181 | + | } | |
| 163 | 182 | if c.active { | |
| 164 | 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 | 210 | func (s *Server) updateConstraint() { | |
| 190 | 211 | var want *constraint | |
| 191 | 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 | 214 | want = c | |
| 194 | 215 | } | |
| 195 | 216 | } | |
| @@ -240,6 +261,17 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 240 | 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 | 275 | // locked reports whether the window's pointer is locked. | |
| 244 | 276 | func (s *Server) locked(w *window) bool { | |
| 245 | 277 | 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 relative | 45 | // 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 | - | return | 50 | + | return false |
| 50 | } | 51 | } | ||
| 51 | if !w.mapped || !w.xdg.resource.Resource.Alive() { | 52 | if !w.mapped || !w.xdg.resource.Resource.Alive() { | ||
| 52 | - | return | 53 | + | 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 a | 65 | // 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 | return | 86 | 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 | return | 101 | 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 nil | 125 | 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] = c | 131 | s.constraints[state] = c | ||
| 122 | return c | 132 | 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 = &r | 158 | 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 = &r | 169 | 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 | return | 177 | return | ||
| 161 | } | 178 | } | ||
| 162 | - | c.region, c.pending = *c.pending, nil | 179 | + | 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 *constraint | 211 | 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 = c | 214 | 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.surface | 277 | 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) { | |||
| 151 | 151 | requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, region, uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot)) | |
| 152 | 152 | // The region is copied: changing it later has no effect. | |
| 153 | 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 | 155 | commands <- ports.FocusWindow{ID: w.ID} | |
| 155 | 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 | 164 | if ev := next(t, c, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventConfined) { | |
| 157 | 165 | t.Fatalf("confine event %v", ev) | |
| 158 | 166 | } | |
| @@ -173,7 +181,7 @@ func TestPointerConfineOneshotRegion(t *testing.T) { | |||
| 173 | 181 | if ev := next(t, c, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventUnconfined) { | |
| 174 | 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 | 185 | if err := c.Roundtrip(); err != nil { | |
| 178 | 186 | t.Fatal(err) | |
| 179 | 187 | } | |
| @@ -189,3 +197,29 @@ func TestPointerConfineOneshotRegion(t *testing.T) { | |||
| 189 | 197 | requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, again, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot)) | |
| 190 | 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 | + | } | |
| @@ -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) { | |||
| 367 | 367 | s.changePointerFocus(c.ID, c.X, c.Y) | |
| 368 | 368 | case ports.PointerMotionTo: | |
| 369 | 369 | 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) { | |
| 373 | 371 | for _, p := range s.windowPointers(w) { | |
| 374 | 372 | pointerFrame(p) | |
| 375 | 373 | } | |
| 374 | + | } | |
| 375 | + | if s.locked(w) { | |
| 376 | + | // Only relative motion reaches a locked pointer. | |
| 376 | 377 | return | |
| 377 | 378 | } | |
| 378 | 379 | x, y := w.surfacePoint(c.X, c.Y) | |
| 379 | 380 | 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 | 385 | for _, p := range s.windowPointers(w) { | |
| 381 | 386 | p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y)) | |
| 382 | 387 | 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 | return | 377 | 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.Y | 380 | 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) { | |||
| 197 | 197 | s.pending = nil | |
| 198 | 198 | s.attached = false | |
| 199 | 199 | } | |
| 200 | - | s.commitConstraint() | |
| 201 | 200 | s.server.awaiting = append(s.server.awaiting, s.callbacks...) | |
| 202 | 201 | s.callbacks = nil | |
| 203 | 202 | if s.role != nil { | |
| @@ -234,6 +233,7 @@ func (s *surface) Commit(*wayland.Surface) { | |||
| 234 | 233 | if s.xdg != nil && s.xdg.pendingGeometry != s.xdg.geometry { | |
| 235 | 234 | s.xdg.geometry, geometry = s.xdg.pendingGeometry, true | |
| 236 | 235 | } | |
| 236 | + | s.commitConstraint(geometry) | |
| 237 | 237 | if fresh || moved || geometry || s.sub.parent != nil { | |
| 238 | 238 | s.redraw() | |
| 239 | 239 | } | |
| @@ -197,7 +197,6 @@ func (s *surface) Commit(*wayland.Surface) { | |||||
| 197 | s.pending = nil | 197 | s.pending = nil | ||
| 198 | s.attached = false | 198 | 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 = nil | 201 | 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, true | 234 | 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() { | |||
| 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 | 449 | return | |
| 449 | 450 | } | |
| 450 | - | c.constraint = g | |
| 451 | + | // A layout change must not leave a locked cursor off its window. | |
| 451 | 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 | 455 | if c.ch.Constraints != nil { | |
| 453 | 456 | latest(c.ch.Constraints, g) | |
| 454 | 457 | } | |
| @@ -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 | return | 449 | return | ||
| 449 | } | 450 | } | ||
| 450 | - | c.constraint = g | 451 | + | // 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) { | |||
| 600 | 600 | client <- ports.PointerConstrained{ID: 1, PointerConstraint: ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 10, Y: 10, W: 1000, H: 20}}} | |
| 601 | 601 | scene(t, scenes) | |
| 602 | 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 | 605 | t.Fatalf("got %+v, want %+v", got, want) | |
| 605 | 606 | } | |
| 606 | 607 | for len(commands) > 0 { | |
| @@ -636,7 +637,7 @@ func TestPointerConstraint(t *testing.T) { | |||
| 636 | 637 | } | |
| 637 | 638 | client <- ports.PointerConstrained{} | |
| 638 | 639 | scene(t, scenes) | |
| 639 | - | if got := receive(t, constraints); got != (ports.PointerConstraint{}) { | |
| 640 | + | if got := receive(t, constraints); got.Mode != ports.ConstraintNone { | |
| 640 | 641 | t.Fatalf("release %+v", got) | |
| 641 | 642 | } | |
| 642 | 643 | } | |
| @@ -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 ( | |||
| 96 | 96 | ||
| 97 | 97 | // PointerConstraint is an active pointer constraint. Rect is logical: in | |
| 98 | 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 | 101 | type PointerConstraint struct { | |
| 101 | 102 | Mode ConstraintMode | |
| 102 | 103 | Rect Rect | |
| 104 | + | X, Y float64 | |
| 103 | 105 | } | |
| 104 | 106 | ||
| 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. | |
| 107 | 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 | 111 | return x, y | |
| 110 | 112 | } | |
| 111 | 113 | 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: in | 97 | // 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 ConstraintMode | 102 | Mode ConstraintMode | ||
| 102 | Rect Rect | 103 | Rect Rect | ||
| 104 | + | X, Y float64 | |||
| 103 | } | 105 | } | ||
| 104 | 106 | ||||
| 105 | - | // Clamp keeps a global point inside a confine rectangle; other modes and an | 107 | + | // 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, y | 111 | 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)) | ||