merge: pointer-games (relative pointer, pointer lock and confine)
14 files · +859 -37
M internal/adapters/libinput/forward.go +6 -1
| @@ -35,7 +35,12 @@ func (f *forwarder) push(ev ports.InputEvent) { | |||
| 35 | 35 | if m, ok := ev.(ports.PointerMotion); ok { | |
| 36 | 36 | f.stats.Motions++ | |
| 37 | 37 | if n := len(f.queue); n > 0 { | |
| 38 | - | if _, tail := f.queue[n-1].(ports.PointerMotion); tail { | |
| 38 | + | if prev, tail := f.queue[n-1].(ports.PointerMotion); tail { | |
| 39 | + | // Relative deltas add up: games read them, not the position. | |
| 40 | + | m.DX += prev.DX | |
| 41 | + | m.DY += prev.DY | |
| 42 | + | m.UnaccelDX += prev.UnaccelDX | |
| 43 | + | m.UnaccelDY += prev.UnaccelDY | |
| 39 | 44 | f.queue[n-1] = m | |
| 40 | 45 | f.stats.Coalesced++ | |
| 41 | 46 | f.mu.Unlock() | |
| @@ -35,7 +35,12 @@ func (f *forwarder) push(ev ports.InputEvent) { | |||||
| 35 | if m, ok := ev.(ports.PointerMotion); ok { | 35 | if m, ok := ev.(ports.PointerMotion); ok { | ||
| 36 | f.stats.Motions++ | 36 | f.stats.Motions++ | ||
| 37 | if n := len(f.queue); n > 0 { | 37 | if n := len(f.queue); n > 0 { | ||
| 38 | - | if _, tail := f.queue[n-1].(ports.PointerMotion); tail { | 38 | + | if prev, tail := f.queue[n-1].(ports.PointerMotion); tail { |
| 39 | + | // Relative deltas add up: games read them, not the position. | |||
| 40 | + | m.DX += prev.DX | |||
| 41 | + | m.DY += prev.DY | |||
| 42 | + | m.UnaccelDX += prev.UnaccelDX | |||
| 43 | + | m.UnaccelDY += prev.UnaccelDY | |||
| 39 | f.queue[n-1] = m | 44 | f.queue[n-1] = m | ||
| 40 | f.stats.Coalesced++ | 45 | f.stats.Coalesced++ | ||
| 41 | f.mu.Unlock() | 46 | f.mu.Unlock() | ||
M internal/adapters/libinput/forward_test.go +11 -0
| @@ -68,3 +68,14 @@ func TestForwarderStopsOnCancel(t *testing.T) { | |||
| 68 | 68 | t.Fatal("run did not stop while blocked on send") | |
| 69 | 69 | } | |
| 70 | 70 | } | |
| 71 | + | ||
| 72 | + | // Coalesced motions keep the sum of their relative deltas. | |
| 73 | + | func TestForwarderSumsDeltas(t *testing.T) { | |
| 74 | + | f := newForwarder() | |
| 75 | + | f.push(ports.PointerMotion{X: 1, DX: 1, DY: 2, UnaccelDX: 3, UnaccelDY: 4}) | |
| 76 | + | f.push(ports.PointerMotion{X: 2, DX: 10, DY: 20, UnaccelDX: 30, UnaccelDY: 40}) | |
| 77 | + | ev, _ := f.pop() | |
| 78 | + | if want := (ports.PointerMotion{X: 2, DX: 11, DY: 22, UnaccelDX: 33, UnaccelDY: 44}); ev != want { | |
| 79 | + | t.Fatalf("got %+v, want %+v", ev, want) | |
| 80 | + | } | |
| 81 | + | } | |
| @@ -68,3 +68,14 @@ func TestForwarderStopsOnCancel(t *testing.T) { | |||||
| 68 | t.Fatal("run did not stop while blocked on send") | 68 | t.Fatal("run did not stop while blocked on send") | ||
| 69 | } | 69 | } | ||
| 70 | } | 70 | } | ||
| 71 | + | ||||
| 72 | + | // Coalesced motions keep the sum of their relative deltas. | |||
| 73 | + | func TestForwarderSumsDeltas(t *testing.T) { | |||
| 74 | + | f := newForwarder() | |||
| 75 | + | f.push(ports.PointerMotion{X: 1, DX: 1, DY: 2, UnaccelDX: 3, UnaccelDY: 4}) | |||
| 76 | + | f.push(ports.PointerMotion{X: 2, DX: 10, DY: 20, UnaccelDX: 30, UnaccelDY: 40}) | |||
| 77 | + | ev, _ := f.pop() | |||
| 78 | + | if want := (ports.PointerMotion{X: 2, DX: 11, DY: 22, UnaccelDX: 33, UnaccelDY: 44}); ev != want { | |||
| 79 | + | t.Fatalf("got %+v, want %+v", ev, want) | |||
| 80 | + | } | |||
| 81 | + | } | |||
M internal/adapters/libinput/libinput.go +45 -10
| @@ -53,6 +53,9 @@ var ( | |||
| 53 | 53 | pointerEvent func(ev uintptr) uintptr | |
| 54 | 54 | pointerDX func(pev uintptr) float64 | |
| 55 | 55 | pointerDY func(pev uintptr) float64 | |
| 56 | + | pointerRawDX func(pev uintptr) float64 | |
| 57 | + | pointerRawDY func(pev uintptr) float64 | |
| 58 | + | pointerUsec func(pev uintptr) uint64 | |
| 56 | 59 | pointerAbsX func(pev uintptr, width uint32) float64 | |
| 57 | 60 | pointerAbsY func(pev uintptr, height uint32) float64 | |
| 58 | 61 | pointerButton func(pev uintptr) uint32 | |
| @@ -99,6 +102,9 @@ func load() error { | |||
| 99 | 102 | reg(&pointerEvent, "event_get_pointer_event") | |
| 100 | 103 | reg(&pointerDX, "event_pointer_get_dx") | |
| 101 | 104 | reg(&pointerDY, "event_pointer_get_dy") | |
| 105 | + | reg(&pointerRawDX, "event_pointer_get_dx_unaccelerated") | |
| 106 | + | reg(&pointerRawDY, "event_pointer_get_dy_unaccelerated") | |
| 107 | + | reg(&pointerUsec, "event_pointer_get_time_usec") | |
| 102 | 108 | reg(&pointerAbsX, "event_pointer_get_absolute_x_transformed") | |
| 103 | 109 | reg(&pointerAbsY, "event_pointer_get_absolute_y_transformed") | |
| 104 | 110 | reg(&pointerButton, "event_pointer_get_button") | |
| @@ -136,7 +142,10 @@ type Options struct { | |||
| 136 | 142 | // Layout is the initial output layout; Layouts replaces it live. | |
| 137 | 143 | Layout ports.Layout | |
| 138 | 144 | Layouts <-chan ports.Layout | |
| 139 | - | Active <-chan bool | |
| 145 | + | // Constraints holds the pointer lock or confinement of the focused | |
| 146 | + | // window, in global logical coordinates. | |
| 147 | + | Constraints <-chan ports.PointerConstraint | |
| 148 | + | Active <-chan bool | |
| 140 | 149 | // MoveCursor, when set, places the hardware cursor as soon as motion is | |
| 141 | 150 | // read, before core sees the event: the output under the pointer and | |
| 142 | 151 | // the physical position on it. | |
| @@ -202,6 +211,9 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error | |||
| 202 | 211 | case l := <-opts.Layouts: | |
| 203 | 212 | p.setLayout(l) | |
| 204 | 213 | p.moved(opts.MoveCursor) | |
| 214 | + | case c := <-opts.Constraints: | |
| 215 | + | p.constrain(c) | |
| 216 | + | p.moved(opts.MoveCursor) | |
| 205 | 217 | default: | |
| 206 | 218 | } | |
| 207 | 219 | fds := []unix.PollFd{{Fd: fd, Events: unix.POLLIN}} | |
| @@ -253,14 +265,19 @@ func translate(ev uintptr, opts Options, p *pointer) (ports.InputEvent, error) { | |||
| 253 | 265 | return ke, nil | |
| 254 | 266 | case evPointerMotion: | |
| 255 | 267 | pe := pointerEvent(ev) | |
| 256 | - | x, y := p.move(pointerDX(pe), pointerDY(pe)) | |
| 268 | + | m := p.move(pointerDX(pe), pointerDY(pe)) | |
| 269 | + | m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe) | |
| 270 | + | m.TimeMsec, m.TimeUsec = now, pointerUsec(pe) | |
| 257 | 271 | p.moved(opts.MoveCursor) | |
| 258 | - | log.Debug().Float64("x", x).Float64("y", y).Msg("pointer") | |
| 259 | - | return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil | |
| 272 | + | log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer") | |
| 273 | + | return m, nil | |
| 260 | 274 | case evPointerAbs: | |
| 261 | 275 | // Absolute devices (tablets, VMs) map to the whole layout. | |
| 262 | 276 | pe := pointerEvent(ev) | |
| 263 | 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 | + | } | |
| 264 | 281 | x, y := p.set(float64(b.X)+pointerAbsX(pe, uint32(b.W)), float64(b.Y)+pointerAbsY(pe, uint32(b.H))) | |
| 265 | 282 | p.moved(opts.MoveCursor) | |
| 266 | 283 | return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil | |
| @@ -310,9 +327,10 @@ func hotkey(ke ports.KeyEvent) (hotkeyAction, int) { | |||
| 310 | 327 | // is divided by the scale of the output under the pointer, so it moves the | |
| 311 | 328 | // same number of physical pixels on every output. | |
| 312 | 329 | type pointer struct { | |
| 313 | - | x, y float64 | |
| 314 | - | layout ports.Layout | |
| 315 | - | touched bool // false until the user moves it: it follows the primary output | |
| 330 | + | x, y float64 | |
| 331 | + | layout ports.Layout | |
| 332 | + | touched bool // false until the user moves it: it follows the primary output | |
| 333 | + | constraint ports.PointerConstraint | |
| 316 | 334 | } | |
| 317 | 335 | ||
| 318 | 336 | // newPointer starts at the centre of the primary output, else the first. | |
| @@ -347,17 +365,34 @@ func (p *pointer) scale() float64 { | |||
| 347 | 365 | return 1 | |
| 348 | 366 | } | |
| 349 | 367 | ||
| 350 | - | func (p *pointer) move(dx, dy float64) (float64, float64) { | |
| 368 | + | // move applies a relative motion and returns it with the logical deltas. | |
| 369 | + | // A locked pointer stays still; a confined one stays in its rectangle. | |
| 370 | + | func (p *pointer) move(dx, dy float64) ports.PointerMotion { | |
| 351 | 371 | s := p.scale() | |
| 352 | - | return p.set(p.x+dx/s, p.y+dy/s) | |
| 372 | + | dx, dy = dx/s, dy/s | |
| 373 | + | if p.constraint.Mode != ports.ConstraintLock { | |
| 374 | + | p.set(p.x+dx, p.y+dy) | |
| 375 | + | } | |
| 376 | + | return ports.PointerMotion{X: p.x, Y: p.y, DX: dx, DY: dy} | |
| 353 | 377 | } | |
| 354 | 378 | ||
| 355 | 379 | func (p *pointer) set(x, y float64) (float64, float64) { | |
| 356 | 380 | p.touched = true | |
| 357 | - | p.x, p.y = p.layout.Clamp(p.x, p.y, x, y) | |
| 381 | + | p.x, p.y = p.constraint.Clamp(p.layout.Clamp(p.x, p.y, x, y)) | |
| 358 | 382 | return p.x, p.y | |
| 359 | 383 | } | |
| 360 | 384 | ||
| 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. | |
| 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 | + | } | |
| 392 | + | p.constraint = c | |
| 393 | + | p.x, p.y = c.Clamp(p.x, p.y) | |
| 394 | + | } | |
| 395 | + | ||
| 361 | 396 | // bounds is the rectangle holding every output. | |
| 362 | 397 | func (p *pointer) bounds() ports.Rect { | |
| 363 | 398 | var r ports.Rect | |
| @@ -53,6 +53,9 @@ var ( | |||||
| 53 | pointerEvent func(ev uintptr) uintptr | 53 | pointerEvent func(ev uintptr) uintptr | ||
| 54 | pointerDX func(pev uintptr) float64 | 54 | pointerDX func(pev uintptr) float64 | ||
| 55 | pointerDY func(pev uintptr) float64 | 55 | pointerDY func(pev uintptr) float64 | ||
| 56 | + | pointerRawDX func(pev uintptr) float64 | |||
| 57 | + | pointerRawDY func(pev uintptr) float64 | |||
| 58 | + | pointerUsec func(pev uintptr) uint64 | |||
| 56 | pointerAbsX func(pev uintptr, width uint32) float64 | 59 | pointerAbsX func(pev uintptr, width uint32) float64 | ||
| 57 | pointerAbsY func(pev uintptr, height uint32) float64 | 60 | pointerAbsY func(pev uintptr, height uint32) float64 | ||
| 58 | pointerButton func(pev uintptr) uint32 | 61 | pointerButton func(pev uintptr) uint32 | ||
| @@ -99,6 +102,9 @@ func load() error { | |||||
| 99 | reg(&pointerEvent, "event_get_pointer_event") | 102 | reg(&pointerEvent, "event_get_pointer_event") | ||
| 100 | reg(&pointerDX, "event_pointer_get_dx") | 103 | reg(&pointerDX, "event_pointer_get_dx") | ||
| 101 | reg(&pointerDY, "event_pointer_get_dy") | 104 | reg(&pointerDY, "event_pointer_get_dy") | ||
| 105 | + | reg(&pointerRawDX, "event_pointer_get_dx_unaccelerated") | |||
| 106 | + | reg(&pointerRawDY, "event_pointer_get_dy_unaccelerated") | |||
| 107 | + | reg(&pointerUsec, "event_pointer_get_time_usec") | |||
| 102 | reg(&pointerAbsX, "event_pointer_get_absolute_x_transformed") | 108 | reg(&pointerAbsX, "event_pointer_get_absolute_x_transformed") | ||
| 103 | reg(&pointerAbsY, "event_pointer_get_absolute_y_transformed") | 109 | reg(&pointerAbsY, "event_pointer_get_absolute_y_transformed") | ||
| 104 | reg(&pointerButton, "event_pointer_get_button") | 110 | reg(&pointerButton, "event_pointer_get_button") | ||
| @@ -136,7 +142,10 @@ type Options struct { | |||||
| 136 | // Layout is the initial output layout; Layouts replaces it live. | 142 | // Layout is the initial output layout; Layouts replaces it live. | ||
| 137 | Layout ports.Layout | 143 | Layout ports.Layout | ||
| 138 | Layouts <-chan ports.Layout | 144 | Layouts <-chan ports.Layout | ||
| 139 | - | Active <-chan bool | 145 | + | // Constraints holds the pointer lock or confinement of the focused |
| 146 | + | // window, in global logical coordinates. | |||
| 147 | + | Constraints <-chan ports.PointerConstraint | |||
| 148 | + | Active <-chan bool | |||
| 140 | // MoveCursor, when set, places the hardware cursor as soon as motion is | 149 | // MoveCursor, when set, places the hardware cursor as soon as motion is | ||
| 141 | // read, before core sees the event: the output under the pointer and | 150 | // read, before core sees the event: the output under the pointer and | ||
| 142 | // the physical position on it. | 151 | // the physical position on it. | ||
| @@ -202,6 +211,9 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error | |||||
| 202 | case l := <-opts.Layouts: | 211 | case l := <-opts.Layouts: | ||
| 203 | p.setLayout(l) | 212 | p.setLayout(l) | ||
| 204 | p.moved(opts.MoveCursor) | 213 | p.moved(opts.MoveCursor) | ||
| 214 | + | case c := <-opts.Constraints: | |||
| 215 | + | p.constrain(c) | |||
| 216 | + | p.moved(opts.MoveCursor) | |||
| 205 | default: | 217 | default: | ||
| 206 | } | 218 | } | ||
| 207 | fds := []unix.PollFd{{Fd: fd, Events: unix.POLLIN}} | 219 | fds := []unix.PollFd{{Fd: fd, Events: unix.POLLIN}} | ||
| @@ -253,14 +265,19 @@ func translate(ev uintptr, opts Options, p *pointer) (ports.InputEvent, error) { | |||||
| 253 | return ke, nil | 265 | return ke, nil | ||
| 254 | case evPointerMotion: | 266 | case evPointerMotion: | ||
| 255 | pe := pointerEvent(ev) | 267 | pe := pointerEvent(ev) | ||
| 256 | - | x, y := p.move(pointerDX(pe), pointerDY(pe)) | 268 | + | m := p.move(pointerDX(pe), pointerDY(pe)) |
| 269 | + | m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe) | |||
| 270 | + | m.TimeMsec, m.TimeUsec = now, pointerUsec(pe) | |||
| 257 | p.moved(opts.MoveCursor) | 271 | p.moved(opts.MoveCursor) | ||
| 258 | - | log.Debug().Float64("x", x).Float64("y", y).Msg("pointer") | 272 | + | log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer") |
| 259 | - | return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil | 273 | + | return m, nil |
| 260 | case evPointerAbs: | 274 | case evPointerAbs: | ||
| 261 | // Absolute devices (tablets, VMs) map to the whole layout. | 275 | // Absolute devices (tablets, VMs) map to the whole layout. | ||
| 262 | pe := pointerEvent(ev) | 276 | pe := pointerEvent(ev) | ||
| 263 | 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 | + | } | |||
| 264 | 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))) | ||
| 265 | p.moved(opts.MoveCursor) | 282 | p.moved(opts.MoveCursor) | ||
| 266 | return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil | 283 | return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil | ||
| @@ -310,9 +327,10 @@ func hotkey(ke ports.KeyEvent) (hotkeyAction, int) { | |||||
| 310 | // is divided by the scale of the output under the pointer, so it moves the | 327 | // is divided by the scale of the output under the pointer, so it moves the | ||
| 311 | // same number of physical pixels on every output. | 328 | // same number of physical pixels on every output. | ||
| 312 | type pointer struct { | 329 | type pointer struct { | ||
| 313 | - | x, y float64 | 330 | + | x, y float64 |
| 314 | - | layout ports.Layout | 331 | + | layout ports.Layout |
| 315 | - | touched bool // false until the user moves it: it follows the primary output | 332 | + | touched bool // false until the user moves it: it follows the primary output |
| 333 | + | constraint ports.PointerConstraint | |||
| 316 | } | 334 | } | ||
| 317 | 335 | ||||
| 318 | // newPointer starts at the centre of the primary output, else the first. | 336 | // newPointer starts at the centre of the primary output, else the first. | ||
| @@ -347,17 +365,34 @@ func (p *pointer) scale() float64 { | |||||
| 347 | return 1 | 365 | return 1 | ||
| 348 | } | 366 | } | ||
| 349 | 367 | ||||
| 350 | - | func (p *pointer) move(dx, dy float64) (float64, float64) { | 368 | + | // move applies a relative motion and returns it with the logical deltas. |
| 369 | + | // A locked pointer stays still; a confined one stays in its rectangle. | |||
| 370 | + | func (p *pointer) move(dx, dy float64) ports.PointerMotion { | |||
| 351 | s := p.scale() | 371 | s := p.scale() | ||
| 352 | - | return p.set(p.x+dx/s, p.y+dy/s) | 372 | + | dx, dy = dx/s, dy/s |
| 373 | + | if p.constraint.Mode != ports.ConstraintLock { | |||
| 374 | + | p.set(p.x+dx, p.y+dy) | |||
| 375 | + | } | |||
| 376 | + | return ports.PointerMotion{X: p.x, Y: p.y, DX: dx, DY: dy} | |||
| 353 | } | 377 | } | ||
| 354 | 378 | ||||
| 355 | func (p *pointer) set(x, y float64) (float64, float64) { | 379 | func (p *pointer) set(x, y float64) (float64, float64) { | ||
| 356 | p.touched = true | 380 | p.touched = true | ||
| 357 | - | p.x, p.y = p.layout.Clamp(p.x, p.y, x, y) | 381 | + | p.x, p.y = p.constraint.Clamp(p.layout.Clamp(p.x, p.y, x, y)) |
| 358 | return p.x, p.y | 382 | return p.x, p.y | ||
| 359 | } | 383 | } | ||
| 360 | 384 | ||||
| 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. | |||
| 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 | + | } | |||
| 392 | + | p.constraint = c | |||
| 393 | + | p.x, p.y = c.Clamp(p.x, p.y) | |||
| 394 | + | } | |||
| 395 | + | ||||
| 361 | // bounds is the rectangle holding every output. | 396 | // bounds is the rectangle holding every output. | ||
| 362 | func (p *pointer) bounds() ports.Rect { | 397 | func (p *pointer) bounds() ports.Rect { | ||
| 363 | var r ports.Rect | 398 | var r ports.Rect | ||
M internal/adapters/libinput/libinput_test.go +34 -4
| @@ -39,13 +39,13 @@ func TestPointerAcrossOutputs(t *testing.T) { | |||
| 39 | 39 | t.Fatal(p.x, p.y) | |
| 40 | 40 | } | |
| 41 | 41 | // Off the bottom of A: clamped to A. | |
| 42 | - | if x, y := p.move(-80, 100); x != 0 || y != 49 { | |
| 43 | - | t.Fatalf("got %v %v", x, y) | |
| 42 | + | if m := p.move(-80, 100); m.X != 0 || m.Y != 49 { | |
| 43 | + | t.Fatalf("got %v %v", m.X, m.Y) | |
| 44 | 44 | } | |
| 45 | 45 | // Into B, where deltas count in physical pixels (scale 2). | |
| 46 | 46 | p.set(150, 20) | |
| 47 | - | if x, y := p.move(20, 10); x != 160 || y != 25 { | |
| 48 | - | t.Fatalf("got %v %v", x, y) | |
| 47 | + | if m := p.move(20, 10); m.X != 160 || m.Y != 25 || m.DX != 10 || m.DY != 5 { | |
| 48 | + | t.Fatalf("got %+v", m) | |
| 49 | 49 | } | |
| 50 | 50 | var out string | |
| 51 | 51 | var px, py float64 | |
| @@ -69,3 +69,33 @@ func TestPointerStartsOnPrimary(t *testing.T) { | |||
| 69 | 69 | t.Fatal(p.x, p.y) | |
| 70 | 70 | } | |
| 71 | 71 | } | |
| 72 | + | ||
| 73 | + | func TestPointerConstraints(t *testing.T) { | |
| 74 | + | p := newPointer(ports.Layout{{Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 100, Scale: 1}}) | |
| 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 { | |
| 77 | + | t.Fatalf("locked: %+v", m) | |
| 78 | + | } | |
| 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}, X: 50, Y: 50}) | |
| 81 | + | if p.x != 60 || p.y != 29 { | |
| 82 | + | t.Fatal(p.x, p.y) | |
| 83 | + | } | |
| 84 | + | if m := p.move(100, 0); m.X != 79 || m.DX != 100 { | |
| 85 | + | t.Fatalf("confined: %+v", m) | |
| 86 | + | } | |
| 87 | + | p.constrain(ports.PointerConstraint{}) | |
| 88 | + | if m := p.move(10, 0); m.X != 89 { | |
| 89 | + | t.Fatalf("free: %+v", m) | |
| 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 | + | } | |
| 101 | + | } | |
| @@ -39,13 +39,13 @@ func TestPointerAcrossOutputs(t *testing.T) { | |||||
| 39 | t.Fatal(p.x, p.y) | 39 | t.Fatal(p.x, p.y) | ||
| 40 | } | 40 | } | ||
| 41 | // Off the bottom of A: clamped to A. | 41 | // Off the bottom of A: clamped to A. | ||
| 42 | - | if x, y := p.move(-80, 100); x != 0 || y != 49 { | 42 | + | if m := p.move(-80, 100); m.X != 0 || m.Y != 49 { |
| 43 | - | t.Fatalf("got %v %v", x, y) | 43 | + | t.Fatalf("got %v %v", m.X, m.Y) |
| 44 | } | 44 | } | ||
| 45 | // Into B, where deltas count in physical pixels (scale 2). | 45 | // Into B, where deltas count in physical pixels (scale 2). | ||
| 46 | p.set(150, 20) | 46 | p.set(150, 20) | ||
| 47 | - | if x, y := p.move(20, 10); x != 160 || y != 25 { | 47 | + | if m := p.move(20, 10); m.X != 160 || m.Y != 25 || m.DX != 10 || m.DY != 5 { |
| 48 | - | t.Fatalf("got %v %v", x, y) | 48 | + | t.Fatalf("got %+v", m) |
| 49 | } | 49 | } | ||
| 50 | var out string | 50 | var out string | ||
| 51 | var px, py float64 | 51 | var px, py float64 | ||
| @@ -69,3 +69,33 @@ func TestPointerStartsOnPrimary(t *testing.T) { | |||||
| 69 | t.Fatal(p.x, p.y) | 69 | t.Fatal(p.x, p.y) | ||
| 70 | } | 70 | } | ||
| 71 | } | 71 | } | ||
| 72 | + | ||||
| 73 | + | func TestPointerConstraints(t *testing.T) { | |||
| 74 | + | p := newPointer(ports.Layout{{Info: ports.OutputInfo{Name: "A"}, Width: 100, Height: 100, Scale: 1}}) | |||
| 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 { | |||
| 77 | + | t.Fatalf("locked: %+v", m) | |||
| 78 | + | } | |||
| 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}, X: 50, Y: 50}) | |||
| 81 | + | if p.x != 60 || p.y != 29 { | |||
| 82 | + | t.Fatal(p.x, p.y) | |||
| 83 | + | } | |||
| 84 | + | if m := p.move(100, 0); m.X != 79 || m.DX != 100 { | |||
| 85 | + | t.Fatalf("confined: %+v", m) | |||
| 86 | + | } | |||
| 87 | + | p.constrain(ports.PointerConstraint{}) | |||
| 88 | + | if m := p.move(10, 0); m.X != 89 { | |||
| 89 | + | t.Fatalf("free: %+v", m) | |||
| 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 | + | } | |||
| 101 | + | } | |||
A internal/adapters/wayland/constraints.go +278 -0
| @@ -0,0 +1,278 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/nefertty/internal/ports" | |
| 5 | + | "github.com/bnema/purego-libwayland/protocol/pointerconstraints" | |
| 6 | + | "github.com/bnema/purego-libwayland/protocol/relativepointer" | |
| 7 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 8 | + | "github.com/bnema/purego-libwayland/server" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func registerPointerConstraints(d *server.Display, s *Server) error { | |
| 12 | + | if err := relativepointer.NewZwpRelativePointerManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |
| 13 | + | _, _ = relativepointer.NewZwpRelativePointerManagerV1(c, int32(v), id, relativeManager{s}) | |
| 14 | + | }); err != nil { | |
| 15 | + | return err | |
| 16 | + | } | |
| 17 | + | return pointerconstraints.NewZwpPointerConstraintsV1Global(d, 1, func(c server.Client, v, id uint32) { | |
| 18 | + | _, _ = pointerconstraints.NewZwpPointerConstraintsV1(c, int32(v), id, constraintManager{s}) | |
| 19 | + | }) | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | type relativeManager struct{ server *Server } | |
| 23 | + | ||
| 24 | + | func (relativeManager) Destroy(*relativepointer.ZwpRelativePointerManagerV1) {} | |
| 25 | + | func (m relativeManager) GetRelativePointer(r *relativepointer.ZwpRelativePointerManagerV1, id uint32, _ *wayland.Pointer) { | |
| 26 | + | rel, err := relativepointer.NewZwpRelativePointerV1(r.Client(), r.Version(), id, relativeHandler{}) | |
| 27 | + | if err != nil { | |
| 28 | + | return | |
| 29 | + | } | |
| 30 | + | s, c := m.server, r.Client() | |
| 31 | + | s.relatives[c] = append(s.relatives[c], rel) | |
| 32 | + | rel.OnDestroy = func() { | |
| 33 | + | if list := removeItem(s.relatives[c], rel); len(list) > 0 { | |
| 34 | + | s.relatives[c] = list | |
| 35 | + | } else { | |
| 36 | + | delete(s.relatives, c) | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | type relativeHandler struct{} | |
| 42 | + | ||
| 43 | + | func (relativeHandler) Destroy(*relativepointer.ZwpRelativePointerV1) {} | |
| 44 | + | ||
| 45 | + | // relativeMotion sends the motion deltas to the window client's relative | |
| 46 | + | // pointers. Absolute devices have no deltas and send nothing. | |
| 47 | + | // It reports whether any event was sent. | |
| 48 | + | func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) bool { | |
| 49 | + | if c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 { | |
| 50 | + | return false | |
| 51 | + | } | |
| 52 | + | if !w.mapped || !w.xdg.resource.Resource.Alive() { | |
| 53 | + | return false | |
| 54 | + | } | |
| 55 | + | sent := false | |
| 56 | + | for _, rel := range s.relatives[w.xdg.resource.Client()] { | |
| 57 | + | if rel.Resource.Alive() { | |
| 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 | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | return sent | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // constraint is a zwp_locked_pointer_v1 or zwp_confined_pointer_v1 on a | |
| 66 | + | // surface. Its region is surface-local; empty means the whole surface. | |
| 67 | + | type constraint struct { | |
| 68 | + | surface *surface | |
| 69 | + | lock *pointerconstraints.ZwpLockedPointerV1 | |
| 70 | + | confine *pointerconstraints.ZwpConfinedPointerV1 | |
| 71 | + | region ports.Rect | |
| 72 | + | pending *ports.Rect // set_region waiting for the surface commit | |
| 73 | + | persistent bool | |
| 74 | + | active bool | |
| 75 | + | defunct bool // a oneshot constraint that was deactivated | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | type constraintManager struct{ server *Server } | |
| 79 | + | ||
| 80 | + | func (constraintManager) Destroy(*pointerconstraints.ZwpPointerConstraintsV1) {} | |
| 81 | + | func (m constraintManager) LockPointer(r *pointerconstraints.ZwpPointerConstraintsV1, id uint32, surf *wayland.Surface, _ *wayland.Pointer, reg *wayland.Region, lifetime uint32) { | |
| 82 | + | c := m.create(r, surf, reg, lifetime) | |
| 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{}}) | |
| 86 | + | return | |
| 87 | + | } | |
| 88 | + | lock, err := pointerconstraints.NewZwpLockedPointerV1(r.Client(), r.Version(), id, lockHandler{c}) | |
| 89 | + | if err != nil { | |
| 90 | + | delete(m.server.constraints, c.surface) | |
| 91 | + | return | |
| 92 | + | } | |
| 93 | + | c.lock = lock | |
| 94 | + | lock.OnDestroy = func() { m.server.dropConstraint(c) } | |
| 95 | + | m.server.updateConstraint() | |
| 96 | + | } | |
| 97 | + | func (m constraintManager) ConfinePointer(r *pointerconstraints.ZwpPointerConstraintsV1, id uint32, surf *wayland.Surface, _ *wayland.Pointer, reg *wayland.Region, lifetime uint32) { | |
| 98 | + | c := m.create(r, surf, reg, lifetime) | |
| 99 | + | if c == nil { | |
| 100 | + | _, _ = pointerconstraints.NewZwpConfinedPointerV1(r.Client(), r.Version(), id, confineHandler{&constraint{}}) | |
| 101 | + | return | |
| 102 | + | } | |
| 103 | + | confine, err := pointerconstraints.NewZwpConfinedPointerV1(r.Client(), r.Version(), id, confineHandler{c}) | |
| 104 | + | if err != nil { | |
| 105 | + | delete(m.server.constraints, c.surface) | |
| 106 | + | return | |
| 107 | + | } | |
| 108 | + | c.confine = confine | |
| 109 | + | confine.OnDestroy = func() { m.server.dropConstraint(c) } | |
| 110 | + | m.server.updateConstraint() | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // create validates the request and registers the constraint on the surface. | |
| 114 | + | func (m constraintManager) create(r *pointerconstraints.ZwpPointerConstraintsV1, surf *wayland.Surface, reg *wayland.Region, lifetime uint32) *constraint { | |
| 115 | + | s := m.server | |
| 116 | + | if surf == nil { | |
| 117 | + | return nil | |
| 118 | + | } | |
| 119 | + | state := s.surfaces[surf.Resource] | |
| 120 | + | if state == nil { | |
| 121 | + | return nil | |
| 122 | + | } | |
| 123 | + | if s.constraints[state] != nil { | |
| 124 | + | r.PostError(uint32(pointerconstraints.ZwpPointerConstraintsV1ErrorAlreadyConstrained), "surface already constrained") | |
| 125 | + | return nil | |
| 126 | + | } | |
| 127 | + | if state.kind == roleCursor { | |
| 128 | + | return nil | |
| 129 | + | } | |
| 130 | + | c := &constraint{surface: state, region: s.regionBox(reg), persistent: lifetime == uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)} | |
| 131 | + | s.constraints[state] = c | |
| 132 | + | return c | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | // regionBox copies a region: the protocol reads it at request time. | |
| 136 | + | func (s *Server) regionBox(reg *wayland.Region) ports.Rect { | |
| 137 | + | if reg == nil || reg.Resource == nil { | |
| 138 | + | return ports.Rect{} | |
| 139 | + | } | |
| 140 | + | if g := s.regions[reg.Resource]; g != nil { | |
| 141 | + | return g.box | |
| 142 | + | } | |
| 143 | + | return ports.Rect{} | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | type lockHandler struct{ c *constraint } | |
| 147 | + | ||
| 148 | + | func (lockHandler) Destroy(*pointerconstraints.ZwpLockedPointerV1) {} | |
| 149 | + | ||
| 150 | + | // SetCursorPositionHint is ignored: the pointer stays where it locked. | |
| 151 | + | func (lockHandler) SetCursorPositionHint(*pointerconstraints.ZwpLockedPointerV1, server.Fixed, server.Fixed) { | |
| 152 | + | } | |
| 153 | + | func (h lockHandler) SetRegion(_ *pointerconstraints.ZwpLockedPointerV1, reg *wayland.Region) { | |
| 154 | + | if h.c.surface == nil { | |
| 155 | + | return | |
| 156 | + | } | |
| 157 | + | r := h.c.surface.server.regionBox(reg) | |
| 158 | + | h.c.pending = &r | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | type confineHandler struct{ c *constraint } | |
| 162 | + | ||
| 163 | + | func (confineHandler) Destroy(*pointerconstraints.ZwpConfinedPointerV1) {} | |
| 164 | + | func (h confineHandler) SetRegion(_ *pointerconstraints.ZwpConfinedPointerV1, reg *wayland.Region) { | |
| 165 | + | if h.c.surface == nil { | |
| 166 | + | return | |
| 167 | + | } | |
| 168 | + | r := h.c.surface.server.regionBox(reg) | |
| 169 | + | h.c.pending = &r | |
| 170 | + | } | |
| 171 | + | ||
| 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) { | |
| 175 | + | c := s.server.constraints[s] | |
| 176 | + | if c == nil || (c.pending == nil && !geometry) { | |
| 177 | + | return | |
| 178 | + | } | |
| 179 | + | if c.pending != nil { | |
| 180 | + | c.region, c.pending = *c.pending, nil | |
| 181 | + | } | |
| 182 | + | if c.active { | |
| 183 | + | s.server.emitConstraint(c) | |
| 184 | + | } else { | |
| 185 | + | s.server.updateConstraint() | |
| 186 | + | } | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | // dropConstraint forgets a destroyed constraint or one on a destroyed surface. | |
| 190 | + | func (s *Server) dropConstraint(c *constraint) { | |
| 191 | + | if s.constraints[c.surface] == c { | |
| 192 | + | delete(s.constraints, c.surface) | |
| 193 | + | } | |
| 194 | + | if s.constraint == c { | |
| 195 | + | c.active = false | |
| 196 | + | s.constraint = nil | |
| 197 | + | if c.lock != nil && c.lock.Resource.Alive() { | |
| 198 | + | c.lock.SendUnlocked() | |
| 199 | + | } | |
| 200 | + | if c.confine != nil && c.confine.Resource.Alive() { | |
| 201 | + | c.confine.SendUnconfined() | |
| 202 | + | } | |
| 203 | + | s.emit(ports.PointerConstrained{}) | |
| 204 | + | } | |
| 205 | + | s.updateConstraint() | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | // updateConstraint activates the constraint of the window holding both | |
| 209 | + | // pointer and keyboard focus, and deactivates any other. | |
| 210 | + | func (s *Server) updateConstraint() { | |
| 211 | + | var want *constraint | |
| 212 | + | if w := s.windows[s.pointerFocus]; w != nil && w.mapped && s.pointerFocus == s.focused { | |
| 213 | + | if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct && (c.active || c.contains(w)) { | |
| 214 | + | want = c | |
| 215 | + | } | |
| 216 | + | } | |
| 217 | + | if want == s.constraint { | |
| 218 | + | return | |
| 219 | + | } | |
| 220 | + | if old := s.constraint; old != nil { | |
| 221 | + | old.active = false | |
| 222 | + | if !old.persistent { | |
| 223 | + | old.defunct = true | |
| 224 | + | } | |
| 225 | + | if old.lock != nil && old.lock.Resource.Alive() { | |
| 226 | + | old.lock.SendUnlocked() | |
| 227 | + | } | |
| 228 | + | if old.confine != nil && old.confine.Resource.Alive() { | |
| 229 | + | old.confine.SendUnconfined() | |
| 230 | + | } | |
| 231 | + | } | |
| 232 | + | s.constraint = want | |
| 233 | + | if want == nil { | |
| 234 | + | s.emit(ports.PointerConstrained{}) | |
| 235 | + | return | |
| 236 | + | } | |
| 237 | + | want.active = true | |
| 238 | + | if want.lock != nil { | |
| 239 | + | want.lock.SendLocked() | |
| 240 | + | } else { | |
| 241 | + | want.confine.SendConfined() | |
| 242 | + | } | |
| 243 | + | s.emitConstraint(want) | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | // emitConstraint tells core about the active constraint, window-local. | |
| 247 | + | func (s *Server) emitConstraint(c *constraint) { | |
| 248 | + | x := c.surface.xdg | |
| 249 | + | if x == nil || x.window == nil { | |
| 250 | + | return | |
| 251 | + | } | |
| 252 | + | mode := ports.ConstraintConfine | |
| 253 | + | if c.lock != nil { | |
| 254 | + | mode = ports.ConstraintLock | |
| 255 | + | } | |
| 256 | + | r := c.region | |
| 257 | + | if r.W > 0 && r.H > 0 { | |
| 258 | + | r.X -= x.geometry.X | |
| 259 | + | r.Y -= x.geometry.Y | |
| 260 | + | } | |
| 261 | + | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | |
| 262 | + | } | |
| 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 | + | ||
| 275 | + | // locked reports whether the window's pointer is locked. | |
| 276 | + | func (s *Server) locked(w *window) bool { | |
| 277 | + | return s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.xdg.surface | |
| 278 | + | } | |
| @@ -0,0 +1,278 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 5 | + | "github.com/bnema/purego-libwayland/protocol/pointerconstraints" | |||
| 6 | + | "github.com/bnema/purego-libwayland/protocol/relativepointer" | |||
| 7 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 8 | + | "github.com/bnema/purego-libwayland/server" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func registerPointerConstraints(d *server.Display, s *Server) error { | |||
| 12 | + | if err := relativepointer.NewZwpRelativePointerManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |||
| 13 | + | _, _ = relativepointer.NewZwpRelativePointerManagerV1(c, int32(v), id, relativeManager{s}) | |||
| 14 | + | }); err != nil { | |||
| 15 | + | return err | |||
| 16 | + | } | |||
| 17 | + | return pointerconstraints.NewZwpPointerConstraintsV1Global(d, 1, func(c server.Client, v, id uint32) { | |||
| 18 | + | _, _ = pointerconstraints.NewZwpPointerConstraintsV1(c, int32(v), id, constraintManager{s}) | |||
| 19 | + | }) | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | type relativeManager struct{ server *Server } | |||
| 23 | + | ||||
| 24 | + | func (relativeManager) Destroy(*relativepointer.ZwpRelativePointerManagerV1) {} | |||
| 25 | + | func (m relativeManager) GetRelativePointer(r *relativepointer.ZwpRelativePointerManagerV1, id uint32, _ *wayland.Pointer) { | |||
| 26 | + | rel, err := relativepointer.NewZwpRelativePointerV1(r.Client(), r.Version(), id, relativeHandler{}) | |||
| 27 | + | if err != nil { | |||
| 28 | + | return | |||
| 29 | + | } | |||
| 30 | + | s, c := m.server, r.Client() | |||
| 31 | + | s.relatives[c] = append(s.relatives[c], rel) | |||
| 32 | + | rel.OnDestroy = func() { | |||
| 33 | + | if list := removeItem(s.relatives[c], rel); len(list) > 0 { | |||
| 34 | + | s.relatives[c] = list | |||
| 35 | + | } else { | |||
| 36 | + | delete(s.relatives, c) | |||
| 37 | + | } | |||
| 38 | + | } | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | type relativeHandler struct{} | |||
| 42 | + | ||||
| 43 | + | func (relativeHandler) Destroy(*relativepointer.ZwpRelativePointerV1) {} | |||
| 44 | + | ||||
| 45 | + | // relativeMotion sends the motion deltas to the window client's relative | |||
| 46 | + | // pointers. Absolute devices have no deltas and send nothing. | |||
| 47 | + | // It reports whether any event was sent. | |||
| 48 | + | func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) bool { | |||
| 49 | + | if c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 { | |||
| 50 | + | return false | |||
| 51 | + | } | |||
| 52 | + | if !w.mapped || !w.xdg.resource.Resource.Alive() { | |||
| 53 | + | return false | |||
| 54 | + | } | |||
| 55 | + | sent := false | |||
| 56 | + | for _, rel := range s.relatives[w.xdg.resource.Client()] { | |||
| 57 | + | if rel.Resource.Alive() { | |||
| 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 | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | return sent | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | // constraint is a zwp_locked_pointer_v1 or zwp_confined_pointer_v1 on a | |||
| 66 | + | // surface. Its region is surface-local; empty means the whole surface. | |||
| 67 | + | type constraint struct { | |||
| 68 | + | surface *surface | |||
| 69 | + | lock *pointerconstraints.ZwpLockedPointerV1 | |||
| 70 | + | confine *pointerconstraints.ZwpConfinedPointerV1 | |||
| 71 | + | region ports.Rect | |||
| 72 | + | pending *ports.Rect // set_region waiting for the surface commit | |||
| 73 | + | persistent bool | |||
| 74 | + | active bool | |||
| 75 | + | defunct bool // a oneshot constraint that was deactivated | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | type constraintManager struct{ server *Server } | |||
| 79 | + | ||||
| 80 | + | func (constraintManager) Destroy(*pointerconstraints.ZwpPointerConstraintsV1) {} | |||
| 81 | + | func (m constraintManager) LockPointer(r *pointerconstraints.ZwpPointerConstraintsV1, id uint32, surf *wayland.Surface, _ *wayland.Pointer, reg *wayland.Region, lifetime uint32) { | |||
| 82 | + | c := m.create(r, surf, reg, lifetime) | |||
| 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{}}) | |||
| 86 | + | return | |||
| 87 | + | } | |||
| 88 | + | lock, err := pointerconstraints.NewZwpLockedPointerV1(r.Client(), r.Version(), id, lockHandler{c}) | |||
| 89 | + | if err != nil { | |||
| 90 | + | delete(m.server.constraints, c.surface) | |||
| 91 | + | return | |||
| 92 | + | } | |||
| 93 | + | c.lock = lock | |||
| 94 | + | lock.OnDestroy = func() { m.server.dropConstraint(c) } | |||
| 95 | + | m.server.updateConstraint() | |||
| 96 | + | } | |||
| 97 | + | func (m constraintManager) ConfinePointer(r *pointerconstraints.ZwpPointerConstraintsV1, id uint32, surf *wayland.Surface, _ *wayland.Pointer, reg *wayland.Region, lifetime uint32) { | |||
| 98 | + | c := m.create(r, surf, reg, lifetime) | |||
| 99 | + | if c == nil { | |||
| 100 | + | _, _ = pointerconstraints.NewZwpConfinedPointerV1(r.Client(), r.Version(), id, confineHandler{&constraint{}}) | |||
| 101 | + | return | |||
| 102 | + | } | |||
| 103 | + | confine, err := pointerconstraints.NewZwpConfinedPointerV1(r.Client(), r.Version(), id, confineHandler{c}) | |||
| 104 | + | if err != nil { | |||
| 105 | + | delete(m.server.constraints, c.surface) | |||
| 106 | + | return | |||
| 107 | + | } | |||
| 108 | + | c.confine = confine | |||
| 109 | + | confine.OnDestroy = func() { m.server.dropConstraint(c) } | |||
| 110 | + | m.server.updateConstraint() | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | // create validates the request and registers the constraint on the surface. | |||
| 114 | + | func (m constraintManager) create(r *pointerconstraints.ZwpPointerConstraintsV1, surf *wayland.Surface, reg *wayland.Region, lifetime uint32) *constraint { | |||
| 115 | + | s := m.server | |||
| 116 | + | if surf == nil { | |||
| 117 | + | return nil | |||
| 118 | + | } | |||
| 119 | + | state := s.surfaces[surf.Resource] | |||
| 120 | + | if state == nil { | |||
| 121 | + | return nil | |||
| 122 | + | } | |||
| 123 | + | if s.constraints[state] != nil { | |||
| 124 | + | r.PostError(uint32(pointerconstraints.ZwpPointerConstraintsV1ErrorAlreadyConstrained), "surface already constrained") | |||
| 125 | + | return nil | |||
| 126 | + | } | |||
| 127 | + | if state.kind == roleCursor { | |||
| 128 | + | return nil | |||
| 129 | + | } | |||
| 130 | + | c := &constraint{surface: state, region: s.regionBox(reg), persistent: lifetime == uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)} | |||
| 131 | + | s.constraints[state] = c | |||
| 132 | + | return c | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | // regionBox copies a region: the protocol reads it at request time. | |||
| 136 | + | func (s *Server) regionBox(reg *wayland.Region) ports.Rect { | |||
| 137 | + | if reg == nil || reg.Resource == nil { | |||
| 138 | + | return ports.Rect{} | |||
| 139 | + | } | |||
| 140 | + | if g := s.regions[reg.Resource]; g != nil { | |||
| 141 | + | return g.box | |||
| 142 | + | } | |||
| 143 | + | return ports.Rect{} | |||
| 144 | + | } | |||
| 145 | + | ||||
| 146 | + | type lockHandler struct{ c *constraint } | |||
| 147 | + | ||||
| 148 | + | func (lockHandler) Destroy(*pointerconstraints.ZwpLockedPointerV1) {} | |||
| 149 | + | ||||
| 150 | + | // SetCursorPositionHint is ignored: the pointer stays where it locked. | |||
| 151 | + | func (lockHandler) SetCursorPositionHint(*pointerconstraints.ZwpLockedPointerV1, server.Fixed, server.Fixed) { | |||
| 152 | + | } | |||
| 153 | + | func (h lockHandler) SetRegion(_ *pointerconstraints.ZwpLockedPointerV1, reg *wayland.Region) { | |||
| 154 | + | if h.c.surface == nil { | |||
| 155 | + | return | |||
| 156 | + | } | |||
| 157 | + | r := h.c.surface.server.regionBox(reg) | |||
| 158 | + | h.c.pending = &r | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | type confineHandler struct{ c *constraint } | |||
| 162 | + | ||||
| 163 | + | func (confineHandler) Destroy(*pointerconstraints.ZwpConfinedPointerV1) {} | |||
| 164 | + | func (h confineHandler) SetRegion(_ *pointerconstraints.ZwpConfinedPointerV1, reg *wayland.Region) { | |||
| 165 | + | if h.c.surface == nil { | |||
| 166 | + | return | |||
| 167 | + | } | |||
| 168 | + | r := h.c.surface.server.regionBox(reg) | |||
| 169 | + | h.c.pending = &r | |||
| 170 | + | } | |||
| 171 | + | ||||
| 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) { | |||
| 175 | + | c := s.server.constraints[s] | |||
| 176 | + | if c == nil || (c.pending == nil && !geometry) { | |||
| 177 | + | return | |||
| 178 | + | } | |||
| 179 | + | if c.pending != nil { | |||
| 180 | + | c.region, c.pending = *c.pending, nil | |||
| 181 | + | } | |||
| 182 | + | if c.active { | |||
| 183 | + | s.server.emitConstraint(c) | |||
| 184 | + | } else { | |||
| 185 | + | s.server.updateConstraint() | |||
| 186 | + | } | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | // dropConstraint forgets a destroyed constraint or one on a destroyed surface. | |||
| 190 | + | func (s *Server) dropConstraint(c *constraint) { | |||
| 191 | + | if s.constraints[c.surface] == c { | |||
| 192 | + | delete(s.constraints, c.surface) | |||
| 193 | + | } | |||
| 194 | + | if s.constraint == c { | |||
| 195 | + | c.active = false | |||
| 196 | + | s.constraint = nil | |||
| 197 | + | if c.lock != nil && c.lock.Resource.Alive() { | |||
| 198 | + | c.lock.SendUnlocked() | |||
| 199 | + | } | |||
| 200 | + | if c.confine != nil && c.confine.Resource.Alive() { | |||
| 201 | + | c.confine.SendUnconfined() | |||
| 202 | + | } | |||
| 203 | + | s.emit(ports.PointerConstrained{}) | |||
| 204 | + | } | |||
| 205 | + | s.updateConstraint() | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | // updateConstraint activates the constraint of the window holding both | |||
| 209 | + | // pointer and keyboard focus, and deactivates any other. | |||
| 210 | + | func (s *Server) updateConstraint() { | |||
| 211 | + | var want *constraint | |||
| 212 | + | if w := s.windows[s.pointerFocus]; w != nil && w.mapped && s.pointerFocus == s.focused { | |||
| 213 | + | if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct && (c.active || c.contains(w)) { | |||
| 214 | + | want = c | |||
| 215 | + | } | |||
| 216 | + | } | |||
| 217 | + | if want == s.constraint { | |||
| 218 | + | return | |||
| 219 | + | } | |||
| 220 | + | if old := s.constraint; old != nil { | |||
| 221 | + | old.active = false | |||
| 222 | + | if !old.persistent { | |||
| 223 | + | old.defunct = true | |||
| 224 | + | } | |||
| 225 | + | if old.lock != nil && old.lock.Resource.Alive() { | |||
| 226 | + | old.lock.SendUnlocked() | |||
| 227 | + | } | |||
| 228 | + | if old.confine != nil && old.confine.Resource.Alive() { | |||
| 229 | + | old.confine.SendUnconfined() | |||
| 230 | + | } | |||
| 231 | + | } | |||
| 232 | + | s.constraint = want | |||
| 233 | + | if want == nil { | |||
| 234 | + | s.emit(ports.PointerConstrained{}) | |||
| 235 | + | return | |||
| 236 | + | } | |||
| 237 | + | want.active = true | |||
| 238 | + | if want.lock != nil { | |||
| 239 | + | want.lock.SendLocked() | |||
| 240 | + | } else { | |||
| 241 | + | want.confine.SendConfined() | |||
| 242 | + | } | |||
| 243 | + | s.emitConstraint(want) | |||
| 244 | + | } | |||
| 245 | + | ||||
| 246 | + | // emitConstraint tells core about the active constraint, window-local. | |||
| 247 | + | func (s *Server) emitConstraint(c *constraint) { | |||
| 248 | + | x := c.surface.xdg | |||
| 249 | + | if x == nil || x.window == nil { | |||
| 250 | + | return | |||
| 251 | + | } | |||
| 252 | + | mode := ports.ConstraintConfine | |||
| 253 | + | if c.lock != nil { | |||
| 254 | + | mode = ports.ConstraintLock | |||
| 255 | + | } | |||
| 256 | + | r := c.region | |||
| 257 | + | if r.W > 0 && r.H > 0 { | |||
| 258 | + | r.X -= x.geometry.X | |||
| 259 | + | r.Y -= x.geometry.Y | |||
| 260 | + | } | |||
| 261 | + | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | |||
| 262 | + | } | |||
| 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 | + | ||||
| 275 | + | // locked reports whether the window's pointer is locked. | |||
| 276 | + | func (s *Server) locked(w *window) bool { | |||
| 277 | + | return s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.xdg.surface | |||
| 278 | + | } | |||
A internal/adapters/wayland/constraints_test.go +234 -0
| @@ -0,0 +1,234 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |
| 8 | + | "github.com/bnema/purego-libwayland/protocol/pointerconstraints" | |
| 9 | + | "github.com/bnema/purego-libwayland/protocol/relativepointer" | |
| 10 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 11 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 12 | + | "github.com/bnema/wlturbo" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // eventLog records the opcodes (and first fixed args) a proxy receives. | |
| 16 | + | type eventLog struct { | |
| 17 | + | wlturbo.BaseProxy | |
| 18 | + | events chan []float64 // opcode, then fixed args for relative motion | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | func (p *eventLog) Dispatch(e *wlturbo.Event) { | |
| 22 | + | ev := []float64{float64(e.Opcode)} | |
| 23 | + | if e.Opcode == uint16(relativepointer.ZwpRelativePointerV1EventRelativeMotion) && e.ProxyID == p.ID() { | |
| 24 | + | e.Uint32() | |
| 25 | + | e.Uint32() | |
| 26 | + | for range 4 { | |
| 27 | + | ev = append(ev, e.Fixed().Float64()) | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | p.events <- ev | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | func logProxy(t *testing.T, c *wlturbo.Display, id uint32) chan []float64 { | |
| 34 | + | t.Helper() | |
| 35 | + | p := &eventLog{events: make(chan []float64, 64)} | |
| 36 | + | p.SetID(id) | |
| 37 | + | c.Context().Register(p) | |
| 38 | + | return p.events | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // next dispatches until the proxy receives an event. | |
| 42 | + | func next(t *testing.T, c *wlturbo.Display, events chan []float64) []float64 { | |
| 43 | + | t.Helper() | |
| 44 | + | deadline := time.Now().Add(2 * time.Second) | |
| 45 | + | for time.Now().Before(deadline) { | |
| 46 | + | select { | |
| 47 | + | case ev := <-events: | |
| 48 | + | return ev | |
| 49 | + | default: | |
| 50 | + | } | |
| 51 | + | if err := c.Roundtrip(); err != nil { | |
| 52 | + | t.Fatal(err) | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | t.Fatal("no event") | |
| 56 | + | return nil | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | func constrained(t *testing.T, events <-chan ports.ClientEvent) ports.PointerConstrained { | |
| 60 | + | t.Helper() | |
| 61 | + | for { | |
| 62 | + | if v, ok := waitEvent(t, events, 2*time.Second).(ports.PointerConstrained); ok { | |
| 63 | + | return v | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | func TestPointerLockAndRelativeMotion(t *testing.T) { | |
| 69 | + | s, events, commands, dir := lifecycleServer(t) | |
| 70 | + | c := protocolClient(t, s, dir) | |
| 71 | + | seat := bindProtocol(t, c, "wl_seat") | |
| 72 | + | registerProtocol(t, c, seat) | |
| 73 | + | pointer := c.AllocateID() | |
| 74 | + | pointerEvents := logProxy(t, c, pointer) | |
| 75 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |
| 76 | + | relManager := bindProtocol(t, c, "zwp_relative_pointer_manager_v1") | |
| 77 | + | rel := c.AllocateID() | |
| 78 | + | relEvents := logProxy(t, c, rel) | |
| 79 | + | requestProtocol(t, c, relManager, relativepointer.ZwpRelativePointerManagerV1RequestGetRelativePointer, rel, pointer) | |
| 80 | + | w, surf, _ := surfaceMapper(t, c, events)() | |
| 81 | + | ||
| 82 | + | constraints := bindProtocol(t, c, "zwp_pointer_constraints_v1") | |
| 83 | + | lock := c.AllocateID() | |
| 84 | + | lockEvents := logProxy(t, c, lock) | |
| 85 | + | requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |
| 86 | + | if err := c.Roundtrip(); err != nil { | |
| 87 | + | t.Fatal(err) | |
| 88 | + | } | |
| 89 | + | select { | |
| 90 | + | case ev := <-lockEvents: | |
| 91 | + | t.Fatalf("locked without focus: %v", ev) | |
| 92 | + | default: | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | // Pointer and keyboard focus activate the lock. | |
| 96 | + | commands <- ports.PointerFocus{ID: w.ID} | |
| 97 | + | commands <- ports.FocusWindow{ID: w.ID} | |
| 98 | + | if ev := next(t, c, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) { | |
| 99 | + | t.Fatalf("lock event %v", ev) | |
| 100 | + | } | |
| 101 | + | if got := constrained(t, events); got.ID != w.ID || got.Mode != ports.ConstraintLock { | |
| 102 | + | t.Fatalf("constrained %+v", got) | |
| 103 | + | } | |
| 104 | + | for len(pointerEvents) > 0 { | |
| 105 | + | <-pointerEvents | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | // Locked: relative motion flows, absolute motion does not. | |
| 109 | + | commands <- ports.PointerMotionTo{ID: w.ID, X: 5, Y: 5, DX: 1.5, DY: -2, UnaccelDX: 3, UnaccelDY: -4} | |
| 110 | + | if ev := next(t, c, relEvents); len(ev) != 5 || ev[1] != 1.5 || ev[2] != -2 || ev[3] != 3 || ev[4] != -4 { | |
| 111 | + | t.Fatalf("relative motion %v", ev) | |
| 112 | + | } | |
| 113 | + | for len(pointerEvents) > 0 { | |
| 114 | + | if ev := <-pointerEvents; ev[0] == float64(wayland.PointerEventMotion) { | |
| 115 | + | t.Fatal("motion sent to a locked pointer") | |
| 116 | + | } | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | // Losing focus unlocks; a persistent lock comes back. | |
| 120 | + | commands <- ports.FocusWindow{} | |
| 121 | + | if ev := next(t, c, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventUnlocked) { | |
| 122 | + | t.Fatalf("unlock event %v", ev) | |
| 123 | + | } | |
| 124 | + | if got := constrained(t, events); got.ID != 0 { | |
| 125 | + | t.Fatalf("constrained %+v", got) | |
| 126 | + | } | |
| 127 | + | commands <- ports.FocusWindow{ID: w.ID} | |
| 128 | + | if ev := next(t, c, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) { | |
| 129 | + | t.Fatalf("relock event %v", ev) | |
| 130 | + | } | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | func TestPointerConfineOneshotRegion(t *testing.T) { | |
| 134 | + | s, events, commands, dir := lifecycleServer(t) | |
| 135 | + | c := protocolClient(t, s, dir) | |
| 136 | + | seat := bindProtocol(t, c, "wl_seat") | |
| 137 | + | registerProtocol(t, c, seat) | |
| 138 | + | pointer := c.AllocateID() | |
| 139 | + | pointerEvents := logProxy(t, c, pointer) | |
| 140 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |
| 141 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 142 | + | w, surf, xdgID := surfaceMapper(t, c, events)() | |
| 143 | + | region := c.AllocateID() | |
| 144 | + | registerProtocol(t, c, region) | |
| 145 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |
| 146 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(2), int32(3), int32(10), int32(10)) | |
| 147 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(20), int32(3), int32(5), int32(20)) | |
| 148 | + | ||
| 149 | + | constraints := bindProtocol(t, c, "zwp_pointer_constraints_v1") | |
| 150 | + | confine := c.AllocateID() | |
| 151 | + | confineEvents := logProxy(t, c, confine) | |
| 152 | + | requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, region, uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot)) | |
| 153 | + | // The region is copied: changing it later has no effect. | |
| 154 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(100), int32(100)) | |
| 155 | + | // Outside the region the constraint waits for the pointer. | |
| 156 | + | commands <- ports.FocusWindow{ID: w.ID} | |
| 157 | + | commands <- ports.PointerFocus{ID: w.ID} | |
| 158 | + | commands <- ports.PointerMotionTo{ID: w.ID, X: 50, Y: 50} | |
| 159 | + | // Sync on the motion reaching the client before checking. | |
| 160 | + | for ev := next(t, c, pointerEvents); ev[0] != float64(wayland.PointerEventMotion); ev = next(t, c, pointerEvents) { | |
| 161 | + | } | |
| 162 | + | if len(confineEvents) > 0 { | |
| 163 | + | t.Fatal("confined outside the region") | |
| 164 | + | } | |
| 165 | + | commands <- ports.PointerMotionTo{ID: w.ID, X: 5, Y: 5} | |
| 166 | + | if ev := next(t, c, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventConfined) { | |
| 167 | + | t.Fatalf("confine event %v", ev) | |
| 168 | + | } | |
| 169 | + | want := ports.PointerConstrained{ID: w.ID, PointerConstraint: ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 2, Y: 3, W: 23, H: 20}}} | |
| 170 | + | if got := constrained(t, events); got != want { | |
| 171 | + | t.Fatalf("constrained %+v, want %+v", got, want) | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | // A geometry change moves the window-local region. | |
| 175 | + | requestProtocol(t, c, xdgID, xdgshell.SurfaceRequestSetWindowGeometry, int32(1), int32(1), int32(1), int32(1)) | |
| 176 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 177 | + | if got := constrained(t, events); got.Rect != (ports.Rect{X: 1, Y: 2, W: 23, H: 20}) { | |
| 178 | + | t.Fatalf("after geometry %+v", got) | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | // A new region applies on commit. | |
| 182 | + | requestProtocol(t, c, confine, pointerconstraints.ZwpConfinedPointerV1RequestSetRegion, uint32(0)) | |
| 183 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 184 | + | if got := constrained(t, events); got.Rect != (ports.Rect{}) { | |
| 185 | + | t.Fatalf("whole surface %+v", got) | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | // A oneshot constraint ends for good with the focus. | |
| 189 | + | commands <- ports.PointerFocus{} | |
| 190 | + | if ev := next(t, c, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventUnconfined) { | |
| 191 | + | t.Fatalf("unconfine event %v", ev) | |
| 192 | + | } | |
| 193 | + | commands <- ports.PointerFocus{ID: w.ID, X: 5, Y: 5} | |
| 194 | + | if err := c.Roundtrip(); err != nil { | |
| 195 | + | t.Fatal(err) | |
| 196 | + | } | |
| 197 | + | select { | |
| 198 | + | case ev := <-confineEvents: | |
| 199 | + | t.Fatalf("oneshot reactivated: %v", ev) | |
| 200 | + | case <-time.After(50 * time.Millisecond): | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // A second constraint on the surface is a protocol error. | |
| 204 | + | again := c.AllocateID() | |
| 205 | + | registerProtocol(t, c, again) | |
| 206 | + | requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, again, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot)) | |
| 207 | + | expectProtocolError(t, c, constraints, uint32(pointerconstraints.ZwpPointerConstraintsV1ErrorAlreadyConstrained)) | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | // Destroying an active lock unlocks the pointer in core. | |
| 211 | + | func TestPointerLockDestroyed(t *testing.T) { | |
| 212 | + | s, events, commands, dir := lifecycleServer(t) | |
| 213 | + | c := protocolClient(t, s, dir) | |
| 214 | + | seat := bindProtocol(t, c, "wl_seat") | |
| 215 | + | registerProtocol(t, c, seat) | |
| 216 | + | pointer := c.AllocateID() | |
| 217 | + | registerProtocol(t, c, pointer) | |
| 218 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |
| 219 | + | w, surf, _ := surfaceMapper(t, c, events)() | |
| 220 | + | constraints := bindProtocol(t, c, "zwp_pointer_constraints_v1") | |
| 221 | + | lock := c.AllocateID() | |
| 222 | + | lockEvents := logProxy(t, c, lock) | |
| 223 | + | requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |
| 224 | + | commands <- ports.PointerFocus{ID: w.ID} | |
| 225 | + | commands <- ports.FocusWindow{ID: w.ID} | |
| 226 | + | next(t, c, lockEvents) | |
| 227 | + | if got := constrained(t, events); got.Mode != ports.ConstraintLock { | |
| 228 | + | t.Fatalf("constrained %+v", got) | |
| 229 | + | } | |
| 230 | + | requestProtocol(t, c, lock, pointerconstraints.ZwpLockedPointerV1RequestDestroy) | |
| 231 | + | if got := constrained(t, events); got.ID != 0 { | |
| 232 | + | t.Fatalf("after destroy %+v", got) | |
| 233 | + | } | |
| 234 | + | } | |
| @@ -0,0 +1,234 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 8 | + | "github.com/bnema/purego-libwayland/protocol/pointerconstraints" | |||
| 9 | + | "github.com/bnema/purego-libwayland/protocol/relativepointer" | |||
| 10 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 11 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |||
| 12 | + | "github.com/bnema/wlturbo" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // eventLog records the opcodes (and first fixed args) a proxy receives. | |||
| 16 | + | type eventLog struct { | |||
| 17 | + | wlturbo.BaseProxy | |||
| 18 | + | events chan []float64 // opcode, then fixed args for relative motion | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | func (p *eventLog) Dispatch(e *wlturbo.Event) { | |||
| 22 | + | ev := []float64{float64(e.Opcode)} | |||
| 23 | + | if e.Opcode == uint16(relativepointer.ZwpRelativePointerV1EventRelativeMotion) && e.ProxyID == p.ID() { | |||
| 24 | + | e.Uint32() | |||
| 25 | + | e.Uint32() | |||
| 26 | + | for range 4 { | |||
| 27 | + | ev = append(ev, e.Fixed().Float64()) | |||
| 28 | + | } | |||
| 29 | + | } | |||
| 30 | + | p.events <- ev | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | func logProxy(t *testing.T, c *wlturbo.Display, id uint32) chan []float64 { | |||
| 34 | + | t.Helper() | |||
| 35 | + | p := &eventLog{events: make(chan []float64, 64)} | |||
| 36 | + | p.SetID(id) | |||
| 37 | + | c.Context().Register(p) | |||
| 38 | + | return p.events | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // next dispatches until the proxy receives an event. | |||
| 42 | + | func next(t *testing.T, c *wlturbo.Display, events chan []float64) []float64 { | |||
| 43 | + | t.Helper() | |||
| 44 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 45 | + | for time.Now().Before(deadline) { | |||
| 46 | + | select { | |||
| 47 | + | case ev := <-events: | |||
| 48 | + | return ev | |||
| 49 | + | default: | |||
| 50 | + | } | |||
| 51 | + | if err := c.Roundtrip(); err != nil { | |||
| 52 | + | t.Fatal(err) | |||
| 53 | + | } | |||
| 54 | + | } | |||
| 55 | + | t.Fatal("no event") | |||
| 56 | + | return nil | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | func constrained(t *testing.T, events <-chan ports.ClientEvent) ports.PointerConstrained { | |||
| 60 | + | t.Helper() | |||
| 61 | + | for { | |||
| 62 | + | if v, ok := waitEvent(t, events, 2*time.Second).(ports.PointerConstrained); ok { | |||
| 63 | + | return v | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | func TestPointerLockAndRelativeMotion(t *testing.T) { | |||
| 69 | + | s, events, commands, dir := lifecycleServer(t) | |||
| 70 | + | c := protocolClient(t, s, dir) | |||
| 71 | + | seat := bindProtocol(t, c, "wl_seat") | |||
| 72 | + | registerProtocol(t, c, seat) | |||
| 73 | + | pointer := c.AllocateID() | |||
| 74 | + | pointerEvents := logProxy(t, c, pointer) | |||
| 75 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 76 | + | relManager := bindProtocol(t, c, "zwp_relative_pointer_manager_v1") | |||
| 77 | + | rel := c.AllocateID() | |||
| 78 | + | relEvents := logProxy(t, c, rel) | |||
| 79 | + | requestProtocol(t, c, relManager, relativepointer.ZwpRelativePointerManagerV1RequestGetRelativePointer, rel, pointer) | |||
| 80 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 81 | + | ||||
| 82 | + | constraints := bindProtocol(t, c, "zwp_pointer_constraints_v1") | |||
| 83 | + | lock := c.AllocateID() | |||
| 84 | + | lockEvents := logProxy(t, c, lock) | |||
| 85 | + | requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |||
| 86 | + | if err := c.Roundtrip(); err != nil { | |||
| 87 | + | t.Fatal(err) | |||
| 88 | + | } | |||
| 89 | + | select { | |||
| 90 | + | case ev := <-lockEvents: | |||
| 91 | + | t.Fatalf("locked without focus: %v", ev) | |||
| 92 | + | default: | |||
| 93 | + | } | |||
| 94 | + | ||||
| 95 | + | // Pointer and keyboard focus activate the lock. | |||
| 96 | + | commands <- ports.PointerFocus{ID: w.ID} | |||
| 97 | + | commands <- ports.FocusWindow{ID: w.ID} | |||
| 98 | + | if ev := next(t, c, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) { | |||
| 99 | + | t.Fatalf("lock event %v", ev) | |||
| 100 | + | } | |||
| 101 | + | if got := constrained(t, events); got.ID != w.ID || got.Mode != ports.ConstraintLock { | |||
| 102 | + | t.Fatalf("constrained %+v", got) | |||
| 103 | + | } | |||
| 104 | + | for len(pointerEvents) > 0 { | |||
| 105 | + | <-pointerEvents | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | // Locked: relative motion flows, absolute motion does not. | |||
| 109 | + | commands <- ports.PointerMotionTo{ID: w.ID, X: 5, Y: 5, DX: 1.5, DY: -2, UnaccelDX: 3, UnaccelDY: -4} | |||
| 110 | + | if ev := next(t, c, relEvents); len(ev) != 5 || ev[1] != 1.5 || ev[2] != -2 || ev[3] != 3 || ev[4] != -4 { | |||
| 111 | + | t.Fatalf("relative motion %v", ev) | |||
| 112 | + | } | |||
| 113 | + | for len(pointerEvents) > 0 { | |||
| 114 | + | if ev := <-pointerEvents; ev[0] == float64(wayland.PointerEventMotion) { | |||
| 115 | + | t.Fatal("motion sent to a locked pointer") | |||
| 116 | + | } | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | // Losing focus unlocks; a persistent lock comes back. | |||
| 120 | + | commands <- ports.FocusWindow{} | |||
| 121 | + | if ev := next(t, c, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventUnlocked) { | |||
| 122 | + | t.Fatalf("unlock event %v", ev) | |||
| 123 | + | } | |||
| 124 | + | if got := constrained(t, events); got.ID != 0 { | |||
| 125 | + | t.Fatalf("constrained %+v", got) | |||
| 126 | + | } | |||
| 127 | + | commands <- ports.FocusWindow{ID: w.ID} | |||
| 128 | + | if ev := next(t, c, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) { | |||
| 129 | + | t.Fatalf("relock event %v", ev) | |||
| 130 | + | } | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | func TestPointerConfineOneshotRegion(t *testing.T) { | |||
| 134 | + | s, events, commands, dir := lifecycleServer(t) | |||
| 135 | + | c := protocolClient(t, s, dir) | |||
| 136 | + | seat := bindProtocol(t, c, "wl_seat") | |||
| 137 | + | registerProtocol(t, c, seat) | |||
| 138 | + | pointer := c.AllocateID() | |||
| 139 | + | pointerEvents := logProxy(t, c, pointer) | |||
| 140 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 141 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 142 | + | w, surf, xdgID := surfaceMapper(t, c, events)() | |||
| 143 | + | region := c.AllocateID() | |||
| 144 | + | registerProtocol(t, c, region) | |||
| 145 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateRegion, region) | |||
| 146 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(2), int32(3), int32(10), int32(10)) | |||
| 147 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(20), int32(3), int32(5), int32(20)) | |||
| 148 | + | ||||
| 149 | + | constraints := bindProtocol(t, c, "zwp_pointer_constraints_v1") | |||
| 150 | + | confine := c.AllocateID() | |||
| 151 | + | confineEvents := logProxy(t, c, confine) | |||
| 152 | + | requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, region, uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot)) | |||
| 153 | + | // The region is copied: changing it later has no effect. | |||
| 154 | + | requestProtocol(t, c, region, wayland.RegionRequestAdd, int32(0), int32(0), int32(100), int32(100)) | |||
| 155 | + | // Outside the region the constraint waits for the pointer. | |||
| 156 | + | commands <- ports.FocusWindow{ID: w.ID} | |||
| 157 | + | commands <- ports.PointerFocus{ID: w.ID} | |||
| 158 | + | commands <- ports.PointerMotionTo{ID: w.ID, X: 50, Y: 50} | |||
| 159 | + | // Sync on the motion reaching the client before checking. | |||
| 160 | + | for ev := next(t, c, pointerEvents); ev[0] != float64(wayland.PointerEventMotion); ev = next(t, c, pointerEvents) { | |||
| 161 | + | } | |||
| 162 | + | if len(confineEvents) > 0 { | |||
| 163 | + | t.Fatal("confined outside the region") | |||
| 164 | + | } | |||
| 165 | + | commands <- ports.PointerMotionTo{ID: w.ID, X: 5, Y: 5} | |||
| 166 | + | if ev := next(t, c, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventConfined) { | |||
| 167 | + | t.Fatalf("confine event %v", ev) | |||
| 168 | + | } | |||
| 169 | + | want := ports.PointerConstrained{ID: w.ID, PointerConstraint: ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 2, Y: 3, W: 23, H: 20}}} | |||
| 170 | + | if got := constrained(t, events); got != want { | |||
| 171 | + | t.Fatalf("constrained %+v, want %+v", got, want) | |||
| 172 | + | } | |||
| 173 | + | ||||
| 174 | + | // A geometry change moves the window-local region. | |||
| 175 | + | requestProtocol(t, c, xdgID, xdgshell.SurfaceRequestSetWindowGeometry, int32(1), int32(1), int32(1), int32(1)) | |||
| 176 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 177 | + | if got := constrained(t, events); got.Rect != (ports.Rect{X: 1, Y: 2, W: 23, H: 20}) { | |||
| 178 | + | t.Fatalf("after geometry %+v", got) | |||
| 179 | + | } | |||
| 180 | + | ||||
| 181 | + | // A new region applies on commit. | |||
| 182 | + | requestProtocol(t, c, confine, pointerconstraints.ZwpConfinedPointerV1RequestSetRegion, uint32(0)) | |||
| 183 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 184 | + | if got := constrained(t, events); got.Rect != (ports.Rect{}) { | |||
| 185 | + | t.Fatalf("whole surface %+v", got) | |||
| 186 | + | } | |||
| 187 | + | ||||
| 188 | + | // A oneshot constraint ends for good with the focus. | |||
| 189 | + | commands <- ports.PointerFocus{} | |||
| 190 | + | if ev := next(t, c, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventUnconfined) { | |||
| 191 | + | t.Fatalf("unconfine event %v", ev) | |||
| 192 | + | } | |||
| 193 | + | commands <- ports.PointerFocus{ID: w.ID, X: 5, Y: 5} | |||
| 194 | + | if err := c.Roundtrip(); err != nil { | |||
| 195 | + | t.Fatal(err) | |||
| 196 | + | } | |||
| 197 | + | select { | |||
| 198 | + | case ev := <-confineEvents: | |||
| 199 | + | t.Fatalf("oneshot reactivated: %v", ev) | |||
| 200 | + | case <-time.After(50 * time.Millisecond): | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | // A second constraint on the surface is a protocol error. | |||
| 204 | + | again := c.AllocateID() | |||
| 205 | + | registerProtocol(t, c, again) | |||
| 206 | + | requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, again, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimeOneshot)) | |||
| 207 | + | expectProtocolError(t, c, constraints, uint32(pointerconstraints.ZwpPointerConstraintsV1ErrorAlreadyConstrained)) | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | // Destroying an active lock unlocks the pointer in core. | |||
| 211 | + | func TestPointerLockDestroyed(t *testing.T) { | |||
| 212 | + | s, events, commands, dir := lifecycleServer(t) | |||
| 213 | + | c := protocolClient(t, s, dir) | |||
| 214 | + | seat := bindProtocol(t, c, "wl_seat") | |||
| 215 | + | registerProtocol(t, c, seat) | |||
| 216 | + | pointer := c.AllocateID() | |||
| 217 | + | registerProtocol(t, c, pointer) | |||
| 218 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 219 | + | w, surf, _ := surfaceMapper(t, c, events)() | |||
| 220 | + | constraints := bindProtocol(t, c, "zwp_pointer_constraints_v1") | |||
| 221 | + | lock := c.AllocateID() | |||
| 222 | + | lockEvents := logProxy(t, c, lock) | |||
| 223 | + | requestProtocol(t, c, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |||
| 224 | + | commands <- ports.PointerFocus{ID: w.ID} | |||
| 225 | + | commands <- ports.FocusWindow{ID: w.ID} | |||
| 226 | + | next(t, c, lockEvents) | |||
| 227 | + | if got := constrained(t, events); got.Mode != ports.ConstraintLock { | |||
| 228 | + | t.Fatalf("constrained %+v", got) | |||
| 229 | + | } | |||
| 230 | + | requestProtocol(t, c, lock, pointerconstraints.ZwpLockedPointerV1RequestDestroy) | |||
| 231 | + | if got := constrained(t, events); got.ID != 0 { | |||
| 232 | + | t.Fatalf("after destroy %+v", got) | |||
| 233 | + | } | |||
| 234 | + | } | |||
M internal/adapters/wayland/globals.go +23 -6
| @@ -16,6 +16,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||
| 16 | 16 | func() error { return registerScale(d, s) }, | |
| 17 | 17 | func() error { return registerCursorShape(d, s) }, | |
| 18 | 18 | func() error { return registerDMABuf(d, s, o.DMABuf) }, | |
| 19 | + | func() error { return registerPointerConstraints(d, s) }, | |
| 19 | 20 | func() error { | |
| 20 | 21 | return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) }) | |
| 21 | 22 | }, | |
| @@ -68,8 +69,12 @@ func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) { | |||
| 68 | 69 | w.OnDestroy = func() { delete(c.server.surfaces, w.Resource); state.Destroy(w) } | |
| 69 | 70 | } | |
| 70 | 71 | } | |
| 71 | - | func (compositor) CreateRegion(r *wayland.Compositor, id uint32) { | |
| 72 | - | wayland.NewRegion(r.Client(), r.Version(), id, region{}) | |
| 72 | + | func (c compositor) CreateRegion(r *wayland.Compositor, id uint32) { | |
| 73 | + | state := ®ion{} | |
| 74 | + | if w, err := wayland.NewRegion(r.Client(), r.Version(), id, state); err == nil { | |
| 75 | + | c.server.regions[w.Resource] = state | |
| 76 | + | w.OnDestroy = func() { delete(c.server.regions, w.Resource) } | |
| 77 | + | } | |
| 73 | 78 | } | |
| 74 | 79 | func (compositor) Release(*wayland.Compositor) {} | |
| 75 | 80 | ||
| @@ -195,11 +200,23 @@ func sortBelowFirst(list []*surface) { | |||
| 195 | 200 | copy(list, append(below, above...)) | |
| 196 | 201 | } | |
| 197 | 202 | ||
| 198 | - | type region struct{} | |
| 203 | + | // region keeps the bounding box of the rectangles added: pointer | |
| 204 | + | // confinement is the only user, and subtracted areas are ignored. | |
| 205 | + | type region struct{ box ports.Rect } | |
| 199 | 206 | ||
| 200 | - | func (region) Destroy(*wayland.Region) {} | |
| 201 | - | func (region) Add(*wayland.Region, int32, int32, int32, int32) {} | |
| 202 | - | func (region) Subtract(*wayland.Region, int32, int32, int32, int32) {} | |
| 207 | + | func (*region) Destroy(*wayland.Region) {} | |
| 208 | + | func (g *region) Add(_ *wayland.Region, x, y, w, h int32) { | |
| 209 | + | if w <= 0 || h <= 0 { | |
| 210 | + | return | |
| 211 | + | } | |
| 212 | + | r := ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)} | |
| 213 | + | if g.box.W > 0 { | |
| 214 | + | x0, y0 := min(g.box.X, r.X), min(g.box.Y, r.Y) | |
| 215 | + | 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} | |
| 216 | + | } | |
| 217 | + | g.box = r | |
| 218 | + | } | |
| 219 | + | func (*region) Subtract(*wayland.Region, int32, int32, int32, int32) {} | |
| 203 | 220 | ||
| 204 | 221 | type shm struct{ server *Server } | |
| 205 | 222 | ||
| @@ -16,6 +16,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||||
| 16 | func() error { return registerScale(d, s) }, | 16 | func() error { return registerScale(d, s) }, | ||
| 17 | func() error { return registerCursorShape(d, s) }, | 17 | func() error { return registerCursorShape(d, s) }, | ||
| 18 | func() error { return registerDMABuf(d, s, o.DMABuf) }, | 18 | func() error { return registerDMABuf(d, s, o.DMABuf) }, | ||
| 19 | + | func() error { return registerPointerConstraints(d, s) }, | |||
| 19 | func() error { | 20 | func() error { | ||
| 20 | return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) }) | 21 | return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) }) | ||
| 21 | }, | 22 | }, | ||
| @@ -68,8 +69,12 @@ func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) { | |||||
| 68 | w.OnDestroy = func() { delete(c.server.surfaces, w.Resource); state.Destroy(w) } | 69 | w.OnDestroy = func() { delete(c.server.surfaces, w.Resource); state.Destroy(w) } | ||
| 69 | } | 70 | } | ||
| 70 | } | 71 | } | ||
| 71 | - | func (compositor) CreateRegion(r *wayland.Compositor, id uint32) { | 72 | + | func (c compositor) CreateRegion(r *wayland.Compositor, id uint32) { |
| 72 | - | wayland.NewRegion(r.Client(), r.Version(), id, region{}) | 73 | + | state := ®ion{} |
| 74 | + | if w, err := wayland.NewRegion(r.Client(), r.Version(), id, state); err == nil { | |||
| 75 | + | c.server.regions[w.Resource] = state | |||
| 76 | + | w.OnDestroy = func() { delete(c.server.regions, w.Resource) } | |||
| 77 | + | } | |||
| 73 | } | 78 | } | ||
| 74 | func (compositor) Release(*wayland.Compositor) {} | 79 | func (compositor) Release(*wayland.Compositor) {} | ||
| 75 | 80 | ||||
| @@ -195,11 +200,23 @@ func sortBelowFirst(list []*surface) { | |||||
| 195 | copy(list, append(below, above...)) | 200 | copy(list, append(below, above...)) | ||
| 196 | } | 201 | } | ||
| 197 | 202 | ||||
| 198 | - | type region struct{} | 203 | + | // region keeps the bounding box of the rectangles added: pointer |
| 204 | + | // confinement is the only user, and subtracted areas are ignored. | |||
| 205 | + | type region struct{ box ports.Rect } | |||
| 199 | 206 | ||||
| 200 | - | func (region) Destroy(*wayland.Region) {} | 207 | + | func (*region) Destroy(*wayland.Region) {} |
| 201 | - | func (region) Add(*wayland.Region, int32, int32, int32, int32) {} | 208 | + | func (g *region) Add(_ *wayland.Region, x, y, w, h int32) { |
| 202 | - | func (region) Subtract(*wayland.Region, int32, int32, int32, int32) {} | 209 | + | if w <= 0 || h <= 0 { |
| 210 | + | return | |||
| 211 | + | } | |||
| 212 | + | r := ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)} | |||
| 213 | + | if g.box.W > 0 { | |||
| 214 | + | x0, y0 := min(g.box.X, r.X), min(g.box.Y, r.Y) | |||
| 215 | + | 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} | |||
| 216 | + | } | |||
| 217 | + | g.box = r | |||
| 218 | + | } | |||
| 219 | + | func (*region) Subtract(*wayland.Region, int32, int32, int32, int32) {} | |||
| 203 | 220 | ||||
| 204 | type shm struct{ server *Server } | 221 | type shm struct{ server *Server } | ||
| 205 | 222 | ||||
M internal/adapters/wayland/protocol_test.go +10 -2
| @@ -590,6 +590,14 @@ func TestHeldKeyOnKeyboardFocusTransfer(t *testing.T) { | |||
| 590 | 590 | // toplevelMapper returns a function that maps a new 1x1 xdg toplevel on c and | |
| 591 | 591 | // returns the WindowMapped event it produced. | |
| 592 | 592 | func toplevelMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent) func() ports.WindowMapped { | |
| 593 | + | t.Helper() | |
| 594 | + | m := surfaceMapper(t, c, events) | |
| 595 | + | return func() ports.WindowMapped { w, _, _ := m(); return w } | |
| 596 | + | } | |
| 597 | + | ||
| 598 | + | // surfaceMapper is toplevelMapper also returning the wl_surface and | |
| 599 | + | // xdg_surface IDs. | |
| 600 | + | func surfaceMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent) func() (ports.WindowMapped, uint32, uint32) { | |
| 593 | 601 | t.Helper() | |
| 594 | 602 | comp := bindProtocol(t, c, "wl_compositor") | |
| 595 | 603 | wm := bindProtocol(t, c, "xdg_wm_base") | |
| @@ -610,7 +618,7 @@ func toplevelMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.Client | |||
| 610 | 618 | t.Fatal(err) | |
| 611 | 619 | } | |
| 612 | 620 | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |
| 613 | - | return func() ports.WindowMapped { | |
| 621 | + | return func() (ports.WindowMapped, uint32, uint32) { | |
| 614 | 622 | surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID() | |
| 615 | 623 | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |
| 616 | 624 | registerProtocol(t, c, surf) | |
| @@ -636,6 +644,6 @@ func toplevelMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.Client | |||
| 636 | 644 | if err := c.Roundtrip(); err != nil { | |
| 637 | 645 | t.Fatal(err) | |
| 638 | 646 | } | |
| 639 | - | return mapped(t, events, 2*time.Second) | |
| 647 | + | return mapped(t, events, 2*time.Second), surf, xdg | |
| 640 | 648 | } | |
| 641 | 649 | } | |
| @@ -590,6 +590,14 @@ func TestHeldKeyOnKeyboardFocusTransfer(t *testing.T) { | |||||
| 590 | // toplevelMapper returns a function that maps a new 1x1 xdg toplevel on c and | 590 | // toplevelMapper returns a function that maps a new 1x1 xdg toplevel on c and | ||
| 591 | // returns the WindowMapped event it produced. | 591 | // returns the WindowMapped event it produced. | ||
| 592 | func toplevelMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent) func() ports.WindowMapped { | 592 | func toplevelMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent) func() ports.WindowMapped { | ||
| 593 | + | t.Helper() | |||
| 594 | + | m := surfaceMapper(t, c, events) | |||
| 595 | + | return func() ports.WindowMapped { w, _, _ := m(); return w } | |||
| 596 | + | } | |||
| 597 | + | ||||
| 598 | + | // surfaceMapper is toplevelMapper also returning the wl_surface and | |||
| 599 | + | // xdg_surface IDs. | |||
| 600 | + | func surfaceMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent) func() (ports.WindowMapped, uint32, uint32) { | |||
| 593 | t.Helper() | 601 | t.Helper() | ||
| 594 | comp := bindProtocol(t, c, "wl_compositor") | 602 | comp := bindProtocol(t, c, "wl_compositor") | ||
| 595 | wm := bindProtocol(t, c, "xdg_wm_base") | 603 | wm := bindProtocol(t, c, "xdg_wm_base") | ||
| @@ -610,7 +618,7 @@ func toplevelMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.Client | |||||
| 610 | t.Fatal(err) | 618 | t.Fatal(err) | ||
| 611 | } | 619 | } | ||
| 612 | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0)) | 620 | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0)) | ||
| 613 | - | return func() ports.WindowMapped { | 621 | + | return func() (ports.WindowMapped, uint32, uint32) { |
| 614 | surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID() | 622 | surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID() | ||
| 615 | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | 623 | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | ||
| 616 | registerProtocol(t, c, surf) | 624 | registerProtocol(t, c, surf) | ||
| @@ -636,6 +644,6 @@ func toplevelMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.Client | |||||
| 636 | if err := c.Roundtrip(); err != nil { | 644 | if err := c.Roundtrip(); err != nil { | ||
| 637 | t.Fatal(err) | 645 | t.Fatal(err) | ||
| 638 | } | 646 | } | ||
| 639 | - | return mapped(t, events, 2*time.Second) | 647 | + | return mapped(t, events, 2*time.Second), surf, xdg |
| 640 | } | 648 | } | ||
| 641 | } | 649 | } | ||
M internal/adapters/wayland/server.go +23 -1
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "encoding/binary" | |
| 6 | 6 | "fmt" | |
| 7 | + | "github.com/bnema/purego-libwayland/protocol/relativepointer" | |
| 7 | 8 | "os" | |
| 8 | 9 | "sync" | |
| 9 | 10 | "time" | |
| @@ -92,6 +93,11 @@ type Server struct { | |||
| 92 | 93 | selections [2]*clipSource | |
| 93 | 94 | clipDevices []*clipDevice | |
| 94 | 95 | dataSources, primarySources, controlSources map[*server.Resource]*clipSource | |
| 96 | + | // Pointer constraints (constraints.go). | |
| 97 | + | regions map[*server.Resource]*region | |
| 98 | + | relatives map[server.Client][]*relativepointer.ZwpRelativePointerV1 | |
| 99 | + | constraints map[*surface]*constraint | |
| 100 | + | constraint *constraint // the active one | |
| 95 | 101 | } | |
| 96 | 102 | ||
| 97 | 103 | func removeItem[T comparable](list []T, v T) []T { | |
| @@ -125,7 +131,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 125 | 131 | d.Close() | |
| 126 | 132 | return nil, err | |
| 127 | 133 | } | |
| 128 | - | s := &Server{display: d, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | |
| 134 | + | s := &Server{display: d, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | |
| 129 | 135 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | |
| 130 | 136 | if opts.Keymap != "" { | |
| 131 | 137 | fd, size, e := keymapFile(opts.Keymap) | |
| @@ -361,7 +367,21 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 361 | 367 | s.changePointerFocus(c.ID, c.X, c.Y) | |
| 362 | 368 | case ports.PointerMotionTo: | |
| 363 | 369 | if w := s.windows[c.ID]; w != nil && c.ID == s.pointerFocus { | |
| 370 | + | if s.relativeMotion(w, c) && s.locked(w) { | |
| 371 | + | for _, p := range s.windowPointers(w) { | |
| 372 | + | pointerFrame(p) | |
| 373 | + | } | |
| 374 | + | } | |
| 375 | + | if s.locked(w) { | |
| 376 | + | // Only relative motion reaches a locked pointer. | |
| 377 | + | return | |
| 378 | + | } | |
| 364 | 379 | x, y := w.surfacePoint(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 | + | } | |
| 365 | 385 | for _, p := range s.windowPointers(w) { | |
| 366 | 386 | p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y)) | |
| 367 | 387 | pointerFrame(p) | |
| @@ -566,6 +586,7 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||
| 566 | 586 | } | |
| 567 | 587 | } | |
| 568 | 588 | s.log.Debug().Uint64("id", uint64(s.focused)).Msg("keyboard focus") | |
| 589 | + | s.updateConstraint() | |
| 569 | 590 | } | |
| 570 | 591 | ||
| 571 | 592 | func pointerFrame(p *wayland.Pointer) { | |
| @@ -613,4 +634,5 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) { | |||
| 613 | 634 | } | |
| 614 | 635 | } | |
| 615 | 636 | } | |
| 637 | + | s.updateConstraint() | |
| 616 | 638 | } | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "encoding/binary" | 5 | "encoding/binary" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | + | "github.com/bnema/purego-libwayland/protocol/relativepointer" | |||
| 7 | "os" | 8 | "os" | ||
| 8 | "sync" | 9 | "sync" | ||
| 9 | "time" | 10 | "time" | ||
| @@ -92,6 +93,11 @@ type Server struct { | |||||
| 92 | selections [2]*clipSource | 93 | selections [2]*clipSource | ||
| 93 | clipDevices []*clipDevice | 94 | clipDevices []*clipDevice | ||
| 94 | dataSources, primarySources, controlSources map[*server.Resource]*clipSource | 95 | dataSources, primarySources, controlSources map[*server.Resource]*clipSource | ||
| 96 | + | // Pointer constraints (constraints.go). | |||
| 97 | + | regions map[*server.Resource]*region | |||
| 98 | + | relatives map[server.Client][]*relativepointer.ZwpRelativePointerV1 | |||
| 99 | + | constraints map[*surface]*constraint | |||
| 100 | + | constraint *constraint // the active one | |||
| 95 | } | 101 | } | ||
| 96 | 102 | ||||
| 97 | func removeItem[T comparable](list []T, v T) []T { | 103 | func removeItem[T comparable](list []T, v T) []T { | ||
| @@ -125,7 +131,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 125 | d.Close() | 131 | d.Close() | ||
| 126 | return nil, err | 132 | return nil, err | ||
| 127 | } | 133 | } | ||
| 128 | - | s := &Server{display: d, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | 134 | + | s := &Server{display: d, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} |
| 129 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | 135 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | ||
| 130 | if opts.Keymap != "" { | 136 | if opts.Keymap != "" { | ||
| 131 | fd, size, e := keymapFile(opts.Keymap) | 137 | fd, size, e := keymapFile(opts.Keymap) | ||
| @@ -361,7 +367,21 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 361 | s.changePointerFocus(c.ID, c.X, c.Y) | 367 | s.changePointerFocus(c.ID, c.X, c.Y) | ||
| 362 | case ports.PointerMotionTo: | 368 | case ports.PointerMotionTo: | ||
| 363 | 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 | + | if s.relativeMotion(w, c) && s.locked(w) { | |||
| 371 | + | for _, p := range s.windowPointers(w) { | |||
| 372 | + | pointerFrame(p) | |||
| 373 | + | } | |||
| 374 | + | } | |||
| 375 | + | if s.locked(w) { | |||
| 376 | + | // Only relative motion reaches a locked pointer. | |||
| 377 | + | return | |||
| 378 | + | } | |||
| 364 | x, y := w.surfacePoint(c.X, c.Y) | 379 | x, y := w.surfacePoint(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 | + | } | |||
| 365 | for _, p := range s.windowPointers(w) { | 385 | for _, p := range s.windowPointers(w) { | ||
| 366 | p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y)) | 386 | p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y)) | ||
| 367 | pointerFrame(p) | 387 | pointerFrame(p) | ||
| @@ -566,6 +586,7 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||||
| 566 | } | 586 | } | ||
| 567 | } | 587 | } | ||
| 568 | s.log.Debug().Uint64("id", uint64(s.focused)).Msg("keyboard focus") | 588 | s.log.Debug().Uint64("id", uint64(s.focused)).Msg("keyboard focus") | ||
| 589 | + | s.updateConstraint() | |||
| 569 | } | 590 | } | ||
| 570 | 591 | ||||
| 571 | func pointerFrame(p *wayland.Pointer) { | 592 | func pointerFrame(p *wayland.Pointer) { | ||
| @@ -613,4 +634,5 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) { | |||||
| 613 | } | 634 | } | ||
| 614 | } | 635 | } | ||
| 615 | } | 636 | } | ||
| 637 | + | s.updateConstraint() | |||
| 616 | } | 638 | } | ||
M internal/adapters/wayland/surface.go +4 -0
| @@ -157,6 +157,9 @@ func (s *surface) Destroy(*wayland.Surface) { | |||
| 157 | 157 | if s.role != nil { | |
| 158 | 158 | s.role(false) | |
| 159 | 159 | } | |
| 160 | + | if c := s.server.constraints[s]; c != nil { | |
| 161 | + | s.server.dropConstraint(c) | |
| 162 | + | } | |
| 160 | 163 | } | |
| 161 | 164 | func (s *surface) Attach(_ *wayland.Surface, b *wayland.Buffer, _, _ int32) { | |
| 162 | 165 | s.pending = b | |
| @@ -230,6 +233,7 @@ func (s *surface) Commit(*wayland.Surface) { | |||
| 230 | 233 | if s.xdg != nil && s.xdg.pendingGeometry != s.xdg.geometry { | |
| 231 | 234 | s.xdg.geometry, geometry = s.xdg.pendingGeometry, true | |
| 232 | 235 | } | |
| 236 | + | s.commitConstraint(geometry) | |
| 233 | 237 | if fresh || moved || geometry || s.sub.parent != nil { | |
| 234 | 238 | s.redraw() | |
| 235 | 239 | } | |
| @@ -157,6 +157,9 @@ func (s *surface) Destroy(*wayland.Surface) { | |||||
| 157 | if s.role != nil { | 157 | if s.role != nil { | ||
| 158 | s.role(false) | 158 | s.role(false) | ||
| 159 | } | 159 | } | ||
| 160 | + | if c := s.server.constraints[s]; c != nil { | |||
| 161 | + | s.server.dropConstraint(c) | |||
| 162 | + | } | |||
| 160 | } | 163 | } | ||
| 161 | func (s *surface) Attach(_ *wayland.Surface, b *wayland.Buffer, _, _ int32) { | 164 | func (s *surface) Attach(_ *wayland.Surface, b *wayland.Buffer, _, _ int32) { | ||
| 162 | s.pending = b | 165 | s.pending = b | ||
| @@ -230,6 +233,7 @@ func (s *surface) Commit(*wayland.Surface) { | |||||
| 230 | if s.xdg != nil && s.xdg.pendingGeometry != s.xdg.geometry { | 233 | if s.xdg != nil && s.xdg.pendingGeometry != s.xdg.geometry { | ||
| 231 | s.xdg.geometry, geometry = s.xdg.pendingGeometry, true | 234 | s.xdg.geometry, geometry = s.xdg.pendingGeometry, true | ||
| 232 | } | 235 | } | ||
| 236 | + | s.commitConstraint(geometry) | |||
| 233 | if fresh || moved || geometry || s.sub.parent != nil { | 237 | if fresh || moved || geometry || s.sub.parent != nil { | ||
| 234 | s.redraw() | 238 | s.redraw() | ||
| 235 | } | 239 | } | ||
M internal/app/app.go +3 -2
| @@ -74,12 +74,13 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 74 | 74 | spawn := make(chan ports.SpawnRequest, 32) | |
| 75 | 75 | scenes := make(chan []ports.Scene, 1) | |
| 76 | 76 | layouts := make(chan ports.Layout, 1) | |
| 77 | + | constraints := make(chan ports.PointerConstraint, 1) | |
| 77 | 78 | states := make(chan ports.State, 1) | |
| 78 | 79 | configErrors := make(chan error, 8) | |
| 79 | 80 | renderScenes := make(chan []ports.Scene, 1) | |
| 80 | 81 | contents := make(chan ports.SurfaceContent, 64) | |
| 81 | 82 | cursorChanges := make(chan ports.CursorChange, 1) | |
| 82 | - | ch := core.Channels{Client: client, Input: input, Output: output, Config: configChanges, Commands: commands, Spawn: spawn, Scenes: scenes, Layouts: layouts, State: states, ConfigErrors: configErrors, Terminal: !opts.NoTerminal} | |
| 83 | + | ch := core.Channels{Client: client, Input: input, Output: output, Config: configChanges, Commands: commands, Spawn: spawn, Scenes: scenes, Layouts: layouts, Constraints: constraints, State: states, ConfigErrors: configErrors, Terminal: !opts.NoTerminal} | |
| 83 | 84 | c, err := core.New(opts.Config, ch) | |
| 84 | 85 | if err != nil { | |
| 85 | 86 | return err | |
| @@ -142,7 +143,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 142 | 143 | return | |
| 143 | 144 | } | |
| 144 | 145 | done <- safe("input", func() error { | |
| 145 | - | return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input")}, input) | |
| 146 | + | return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input")}, input) | |
| 146 | 147 | }) | |
| 147 | 148 | return | |
| 148 | 149 | } | |
| @@ -74,12 +74,13 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 74 | spawn := make(chan ports.SpawnRequest, 32) | 74 | spawn := make(chan ports.SpawnRequest, 32) | ||
| 75 | scenes := make(chan []ports.Scene, 1) | 75 | scenes := make(chan []ports.Scene, 1) | ||
| 76 | layouts := make(chan ports.Layout, 1) | 76 | layouts := make(chan ports.Layout, 1) | ||
| 77 | + | constraints := make(chan ports.PointerConstraint, 1) | |||
| 77 | states := make(chan ports.State, 1) | 78 | states := make(chan ports.State, 1) | ||
| 78 | configErrors := make(chan error, 8) | 79 | configErrors := make(chan error, 8) | ||
| 79 | renderScenes := make(chan []ports.Scene, 1) | 80 | renderScenes := make(chan []ports.Scene, 1) | ||
| 80 | contents := make(chan ports.SurfaceContent, 64) | 81 | contents := make(chan ports.SurfaceContent, 64) | ||
| 81 | cursorChanges := make(chan ports.CursorChange, 1) | 82 | cursorChanges := make(chan ports.CursorChange, 1) | ||
| 82 | - | ch := core.Channels{Client: client, Input: input, Output: output, Config: configChanges, Commands: commands, Spawn: spawn, Scenes: scenes, Layouts: layouts, State: states, ConfigErrors: configErrors, Terminal: !opts.NoTerminal} | 83 | + | ch := core.Channels{Client: client, Input: input, Output: output, Config: configChanges, Commands: commands, Spawn: spawn, Scenes: scenes, Layouts: layouts, Constraints: constraints, State: states, ConfigErrors: configErrors, Terminal: !opts.NoTerminal} |
| 83 | c, err := core.New(opts.Config, ch) | 84 | c, err := core.New(opts.Config, ch) | ||
| 84 | if err != nil { | 85 | if err != nil { | ||
| 85 | return err | 86 | return err | ||
| @@ -142,7 +143,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 142 | return | 143 | return | ||
| 143 | } | 144 | } | ||
| 144 | done <- safe("input", func() error { | 145 | done <- safe("input", func() error { | ||
| 145 | - | return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input")}, input) | 146 | + | return libinput.Run(ctx, libinput.Options{Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input")}, input) |
| 146 | }) | 147 | }) | ||
| 147 | return | 148 | return | ||
| 148 | } | 149 | } | ||
M internal/core/core.go +57 -6
| @@ -26,7 +26,11 @@ type Channels struct { | |||
| 26 | 26 | Scenes chan []ports.Scene | |
| 27 | 27 | // Layouts, when set, receives the output layout whenever it changes, | |
| 28 | 28 | // latest first (capacity 1, drained like Scenes). Input and cursors use it. | |
| 29 | - | Layouts chan ports.Layout | |
| 29 | + | Layouts chan ports.Layout | |
| 30 | + | // Constraints, when set, receives the active pointer constraint in | |
| 31 | + | // global logical coordinates whenever it changes (capacity 1, drained | |
| 32 | + | // like Scenes). Input applies it to the pointer. | |
| 33 | + | Constraints chan ports.PointerConstraint | |
| 30 | 34 | ConfigErrors chan<- error | |
| 31 | 35 | // State, when set, receives a snapshot for scripts whenever it changes, | |
| 32 | 36 | // latest first (capacity 1, drained like Scenes). | |
| @@ -53,7 +57,11 @@ type Core struct { | |||
| 53 | 57 | buttons map[uint32]bool | |
| 54 | 58 | cursorX, cursorY float64 // global, logical | |
| 55 | 59 | pointerOutput string // output under the pointer at the last motion | |
| 56 | - | seq uint64 | |
| 60 | + | // constrained is the constraint wayland activated; constraint is it | |
| 61 | + | // resolved to global coordinates, as last sent to input. | |
| 62 | + | constrained ports.PointerConstrained | |
| 63 | + | constraint ports.PointerConstraint | |
| 64 | + | seq uint64 | |
| 57 | 65 | // layerChanged is set once layer state arrives from wayland. | |
| 58 | 66 | layerChanged bool | |
| 59 | 67 | sentOutputs ports.SetOutputs | |
| @@ -248,8 +256,8 @@ func (c *Core) spawnSlots(ctx context.Context, shown bool) error { | |||
| 248 | 256 | return nil | |
| 249 | 257 | } | |
| 250 | 258 | func New(cfg ports.Config, ch Channels) (*Core, error) { | |
| 251 | - | if cap(ch.Scenes) != 1 || (ch.Layouts != nil && cap(ch.Layouts) != 1) || (ch.State != nil && cap(ch.State) != 1) { | |
| 252 | - | return nil, fmt.Errorf("scenes, layouts and state must have capacity 1") | |
| 259 | + | if cap(ch.Scenes) != 1 || (ch.Layouts != nil && cap(ch.Layouts) != 1) || (ch.Constraints != nil && cap(ch.Constraints) != 1) || (ch.State != nil && cap(ch.State) != 1) { | |
| 260 | + | return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1") | |
| 253 | 261 | } | |
| 254 | 262 | // A placeholder screen holds windows until the first output arrives. | |
| 255 | 263 | c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}} | |
| @@ -406,11 +414,49 @@ func (c *Core) publish(ctx context.Context) error { | |||
| 406 | 414 | } | |
| 407 | 415 | c.focus = focus | |
| 408 | 416 | } | |
| 417 | + | c.resolveConstraint() | |
| 409 | 418 | latest(c.ch.Scenes, scenes) | |
| 410 | 419 | c.publishState() | |
| 411 | 420 | return nil | |
| 412 | 421 | } | |
| 413 | 422 | ||
| 423 | + | // resolveConstraint turns the active constraint into global coordinates | |
| 424 | + | // and sends it to input when it changed. A hidden window holds nothing. | |
| 425 | + | func (c *Core) resolveConstraint() { | |
| 426 | + | var g ports.PointerConstraint | |
| 427 | + | if id := c.constrained.ID; id != 0 { | |
| 428 | + | if s, _ := c.screenOf(id); s != nil { | |
| 429 | + | for _, p := range s.mon.Layout() { | |
| 430 | + | if p.ID != id || p.Hidden { | |
| 431 | + | continue | |
| 432 | + | } | |
| 433 | + | r := c.clientRect(p) | |
| 434 | + | r.X += s.x | |
| 435 | + | if w := c.constrained.Rect; w.W > 0 && w.H > 0 { | |
| 436 | + | // The region is clipped to the window. | |
| 437 | + | x0, y0 := max(r.X, r.X+w.X), max(r.Y, r.Y+w.Y) | |
| 438 | + | x1, y1 := min(r.X+r.W, r.X+w.X+w.W), min(r.Y+r.H, r.Y+w.Y+w.H) | |
| 439 | + | if x1 > x0 && y1 > y0 { | |
| 440 | + | r = Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0} | |
| 441 | + | } | |
| 442 | + | } | |
| 443 | + | g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: r} | |
| 444 | + | } | |
| 445 | + | } | |
| 446 | + | } | |
| 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 { | |
| 449 | + | return | |
| 450 | + | } | |
| 451 | + | // A layout change must not leave a locked cursor off its window. | |
| 452 | + | c.cursorX, c.cursorY = g.Clamp(c.cursorX, c.cursorY) | |
| 453 | + | g.X, g.Y = c.cursorX, c.cursorY | |
| 454 | + | c.constraint = g | |
| 455 | + | if c.ch.Constraints != nil { | |
| 456 | + | latest(c.ch.Constraints, g) | |
| 457 | + | } | |
| 458 | + | } | |
| 459 | + | ||
| 414 | 460 | // latest replaces any unread value: only the owner sends and drains; | |
| 415 | 461 | // consumers only receive. | |
| 416 | 462 | func latest[T any](ch chan T, v T) { | |
| @@ -517,6 +563,8 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 517 | 563 | return nil | |
| 518 | 564 | } | |
| 519 | 565 | } | |
| 566 | + | case ports.PointerConstrained: | |
| 567 | + | c.constrained = v | |
| 520 | 568 | case ports.WindowFullscreenRequest: | |
| 521 | 569 | if s, _ := c.screenOf(v.ID); s != nil { | |
| 522 | 570 | s.mon.SetFullscreen(v.ID, v.Fullscreen) | |
| @@ -571,7 +619,10 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 571 | 619 | } | |
| 572 | 620 | switch v := ev.(type) { | |
| 573 | 621 | case ports.PointerMotion: | |
| 574 | - | c.cursorX, c.cursorY = c.layout().Clamp(c.cursorX, c.cursorY, v.X, v.Y) | |
| 622 | + | // A locked pointer stays still; relative motion still flows. | |
| 623 | + | if c.constraint.Mode != ports.ConstraintLock { | |
| 624 | + | c.cursorX, c.cursorY = c.constraint.Clamp(c.layout().Clamp(c.cursorX, c.cursorY, v.X, v.Y)) | |
| 625 | + | } | |
| 575 | 626 | // The focused screen follows the pointer, so new windows | |
| 576 | 627 | // and launchers open where the user is. | |
| 577 | 628 | // Only a pointer entering another output switches: keyboard | |
| @@ -594,7 +645,7 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 594 | 645 | } | |
| 595 | 646 | } | |
| 596 | 647 | if id != 0 { | |
| 597 | - | if err := c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, TimeMsec: v.TimeMsec}); err != nil { | |
| 648 | + | if err := c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, TimeMsec: v.TimeMsec, TimeUsec: v.TimeUsec}); err != nil { | |
| 598 | 649 | return nil | |
| 599 | 650 | } | |
| 600 | 651 | } | |
| @@ -26,7 +26,11 @@ type Channels struct { | |||||
| 26 | Scenes chan []ports.Scene | 26 | Scenes chan []ports.Scene | ||
| 27 | // Layouts, when set, receives the output layout whenever it changes, | 27 | // Layouts, when set, receives the output layout whenever it changes, | ||
| 28 | // latest first (capacity 1, drained like Scenes). Input and cursors use it. | 28 | // latest first (capacity 1, drained like Scenes). Input and cursors use it. | ||
| 29 | - | Layouts chan ports.Layout | 29 | + | Layouts chan ports.Layout |
| 30 | + | // Constraints, when set, receives the active pointer constraint in | |||
| 31 | + | // global logical coordinates whenever it changes (capacity 1, drained | |||
| 32 | + | // like Scenes). Input applies it to the pointer. | |||
| 33 | + | Constraints chan ports.PointerConstraint | |||
| 30 | ConfigErrors chan<- error | 34 | ConfigErrors chan<- error | ||
| 31 | // State, when set, receives a snapshot for scripts whenever it changes, | 35 | // State, when set, receives a snapshot for scripts whenever it changes, | ||
| 32 | // latest first (capacity 1, drained like Scenes). | 36 | // latest first (capacity 1, drained like Scenes). | ||
| @@ -53,7 +57,11 @@ type Core struct { | |||||
| 53 | buttons map[uint32]bool | 57 | buttons map[uint32]bool | ||
| 54 | cursorX, cursorY float64 // global, logical | 58 | cursorX, cursorY float64 // global, logical | ||
| 55 | pointerOutput string // output under the pointer at the last motion | 59 | pointerOutput string // output under the pointer at the last motion | ||
| 56 | - | seq uint64 | 60 | + | // constrained is the constraint wayland activated; constraint is it |
| 61 | + | // resolved to global coordinates, as last sent to input. | |||
| 62 | + | constrained ports.PointerConstrained | |||
| 63 | + | constraint ports.PointerConstraint | |||
| 64 | + | seq uint64 | |||
| 57 | // layerChanged is set once layer state arrives from wayland. | 65 | // layerChanged is set once layer state arrives from wayland. | ||
| 58 | layerChanged bool | 66 | layerChanged bool | ||
| 59 | sentOutputs ports.SetOutputs | 67 | sentOutputs ports.SetOutputs | ||
| @@ -248,8 +256,8 @@ func (c *Core) spawnSlots(ctx context.Context, shown bool) error { | |||||
| 248 | return nil | 256 | return nil | ||
| 249 | } | 257 | } | ||
| 250 | func New(cfg ports.Config, ch Channels) (*Core, error) { | 258 | func New(cfg ports.Config, ch Channels) (*Core, error) { | ||
| 251 | - | if cap(ch.Scenes) != 1 || (ch.Layouts != nil && cap(ch.Layouts) != 1) || (ch.State != nil && cap(ch.State) != 1) { | 259 | + | if cap(ch.Scenes) != 1 || (ch.Layouts != nil && cap(ch.Layouts) != 1) || (ch.Constraints != nil && cap(ch.Constraints) != 1) || (ch.State != nil && cap(ch.State) != 1) { |
| 252 | - | return nil, fmt.Errorf("scenes, layouts and state must have capacity 1") | 260 | + | return nil, fmt.Errorf("scenes, layouts, constraints and state must have capacity 1") |
| 253 | } | 261 | } | ||
| 254 | // A placeholder screen holds windows until the first output arrives. | 262 | // A placeholder screen holds windows until the first output arrives. | ||
| 255 | c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}} | 263 | c := &Core{screens: []*screen{{mon: NewMonitor(), scale: 1, cfgScale: 1}}, slots: map[slotKey]*slotState{}, terms: map[string]*termSpawn{}, clients: map[WindowID]ports.WindowMapped{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, sent: map[WindowID]ports.ConfigureWindow{}} | ||
| @@ -406,11 +414,49 @@ func (c *Core) publish(ctx context.Context) error { | |||||
| 406 | } | 414 | } | ||
| 407 | c.focus = focus | 415 | c.focus = focus | ||
| 408 | } | 416 | } | ||
| 417 | + | c.resolveConstraint() | |||
| 409 | latest(c.ch.Scenes, scenes) | 418 | latest(c.ch.Scenes, scenes) | ||
| 410 | c.publishState() | 419 | c.publishState() | ||
| 411 | return nil | 420 | return nil | ||
| 412 | } | 421 | } | ||
| 413 | 422 | ||||
| 423 | + | // resolveConstraint turns the active constraint into global coordinates | |||
| 424 | + | // and sends it to input when it changed. A hidden window holds nothing. | |||
| 425 | + | func (c *Core) resolveConstraint() { | |||
| 426 | + | var g ports.PointerConstraint | |||
| 427 | + | if id := c.constrained.ID; id != 0 { | |||
| 428 | + | if s, _ := c.screenOf(id); s != nil { | |||
| 429 | + | for _, p := range s.mon.Layout() { | |||
| 430 | + | if p.ID != id || p.Hidden { | |||
| 431 | + | continue | |||
| 432 | + | } | |||
| 433 | + | r := c.clientRect(p) | |||
| 434 | + | r.X += s.x | |||
| 435 | + | if w := c.constrained.Rect; w.W > 0 && w.H > 0 { | |||
| 436 | + | // The region is clipped to the window. | |||
| 437 | + | x0, y0 := max(r.X, r.X+w.X), max(r.Y, r.Y+w.Y) | |||
| 438 | + | x1, y1 := min(r.X+r.W, r.X+w.X+w.W), min(r.Y+r.H, r.Y+w.Y+w.H) | |||
| 439 | + | if x1 > x0 && y1 > y0 { | |||
| 440 | + | r = Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0} | |||
| 441 | + | } | |||
| 442 | + | } | |||
| 443 | + | g = ports.PointerConstraint{Mode: c.constrained.Mode, Rect: r} | |||
| 444 | + | } | |||
| 445 | + | } | |||
| 446 | + | } | |||
| 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 { | |||
| 449 | + | return | |||
| 450 | + | } | |||
| 451 | + | // A layout change must not leave a locked cursor off its window. | |||
| 452 | + | c.cursorX, c.cursorY = g.Clamp(c.cursorX, c.cursorY) | |||
| 453 | + | g.X, g.Y = c.cursorX, c.cursorY | |||
| 454 | + | c.constraint = g | |||
| 455 | + | if c.ch.Constraints != nil { | |||
| 456 | + | latest(c.ch.Constraints, g) | |||
| 457 | + | } | |||
| 458 | + | } | |||
| 459 | + | ||||
| 414 | // latest replaces any unread value: only the owner sends and drains; | 460 | // latest replaces any unread value: only the owner sends and drains; | ||
| 415 | // consumers only receive. | 461 | // consumers only receive. | ||
| 416 | func latest[T any](ch chan T, v T) { | 462 | func latest[T any](ch chan T, v T) { | ||
| @@ -517,6 +563,8 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 517 | return nil | 563 | return nil | ||
| 518 | } | 564 | } | ||
| 519 | } | 565 | } | ||
| 566 | + | case ports.PointerConstrained: | |||
| 567 | + | c.constrained = v | |||
| 520 | case ports.WindowFullscreenRequest: | 568 | case ports.WindowFullscreenRequest: | ||
| 521 | if s, _ := c.screenOf(v.ID); s != nil { | 569 | if s, _ := c.screenOf(v.ID); s != nil { | ||
| 522 | s.mon.SetFullscreen(v.ID, v.Fullscreen) | 570 | s.mon.SetFullscreen(v.ID, v.Fullscreen) | ||
| @@ -571,7 +619,10 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 571 | } | 619 | } | ||
| 572 | switch v := ev.(type) { | 620 | switch v := ev.(type) { | ||
| 573 | case ports.PointerMotion: | 621 | case ports.PointerMotion: | ||
| 574 | - | c.cursorX, c.cursorY = c.layout().Clamp(c.cursorX, c.cursorY, v.X, v.Y) | 622 | + | // A locked pointer stays still; relative motion still flows. |
| 623 | + | if c.constraint.Mode != ports.ConstraintLock { | |||
| 624 | + | c.cursorX, c.cursorY = c.constraint.Clamp(c.layout().Clamp(c.cursorX, c.cursorY, v.X, v.Y)) | |||
| 625 | + | } | |||
| 575 | // The focused screen follows the pointer, so new windows | 626 | // The focused screen follows the pointer, so new windows | ||
| 576 | // and launchers open where the user is. | 627 | // and launchers open where the user is. | ||
| 577 | // Only a pointer entering another output switches: keyboard | 628 | // Only a pointer entering another output switches: keyboard | ||
| @@ -594,7 +645,7 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 594 | } | 645 | } | ||
| 595 | } | 646 | } | ||
| 596 | if id != 0 { | 647 | if id != 0 { | ||
| 597 | - | if err := c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, TimeMsec: v.TimeMsec}); err != nil { | 648 | + | if err := c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, TimeMsec: v.TimeMsec, TimeUsec: v.TimeUsec}); err != nil { |
| 598 | return nil | 649 | return nil | ||
| 599 | } | 650 | } | ||
| 600 | } | 651 | } | ||
M internal/core/core_test.go +71 -0
| @@ -570,3 +570,74 @@ func TestOutputScale(t *testing.T) { | |||
| 570 | 570 | t.Fatal(s.Scale, s.OutputWidth) | |
| 571 | 571 | } | |
| 572 | 572 | } | |
| 573 | + | ||
| 574 | + | func TestPointerConstraint(t *testing.T) { | |
| 575 | + | cfg := config.Defaults() | |
| 576 | + | cfg.Border.Width = 0 | |
| 577 | + | cfg.Layout.Gaps = 0 | |
| 578 | + | client := make(chan ports.ClientEvent, 8) | |
| 579 | + | input := make(chan ports.InputEvent, 8) | |
| 580 | + | output := make(chan ports.OutputEvent, 8) | |
| 581 | + | commands := make(chan ports.ClientCommand, 64) | |
| 582 | + | scenes := make(chan []ports.Scene, 1) | |
| 583 | + | constraints := make(chan ports.PointerConstraint, 1) | |
| 584 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Constraints: constraints}) | |
| 585 | + | if err != nil { | |
| 586 | + | t.Fatal(err) | |
| 587 | + | } | |
| 588 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 589 | + | defer cancel() | |
| 590 | + | go c.Run(ctx) | |
| 591 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "A", Width: 100, Height: 80}} | |
| 592 | + | scene(t, scenes) | |
| 593 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "B", Width: 100, Height: 80}} | |
| 594 | + | scene(t, scenes) | |
| 595 | + | input <- ports.PointerMotion{X: 150, Y: 40} | |
| 596 | + | scene(t, scenes) // the pointer moves the focus to B | |
| 597 | + | client <- ports.WindowMapped{ID: 1} | |
| 598 | + | r := receive(t, scenes)[1].Windows[0].Rect | |
| 599 | + | // The confine region is window-local and clipped to the window. | |
| 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) | |
| 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 | + | // 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 { | |
| 605 | + | t.Fatalf("got %+v, want %+v", got, want) | |
| 606 | + | } | |
| 607 | + | for len(commands) > 0 { | |
| 608 | + | <-commands | |
| 609 | + | } | |
| 610 | + | // Motion is clamped into the region; deltas pass through. | |
| 611 | + | input <- ports.PointerMotion{X: 199, Y: 79, DX: 5, DY: 6} | |
| 612 | + | for { | |
| 613 | + | if v, ok := command(t, commands).(ports.PointerMotionTo); ok { | |
| 614 | + | if v.X != float64(want.Rect.X+want.Rect.W-1-100-r.X) || v.Y != float64(want.Rect.Y+want.Rect.H-1-r.Y) || v.DX != 5 || v.DY != 6 { | |
| 615 | + | t.Fatalf("motion %+v", v) | |
| 616 | + | } | |
| 617 | + | break | |
| 618 | + | } | |
| 619 | + | } | |
| 620 | + | // A lock keeps the pointer still. | |
| 621 | + | client <- ports.PointerConstrained{ID: 1, PointerConstraint: ports.PointerConstraint{Mode: ports.ConstraintLock}} | |
| 622 | + | scene(t, scenes) | |
| 623 | + | if got := receive(t, constraints); got.Mode != ports.ConstraintLock { | |
| 624 | + | t.Fatalf("lock %+v", got) | |
| 625 | + | } | |
| 626 | + | for len(commands) > 0 { | |
| 627 | + | <-commands | |
| 628 | + | } | |
| 629 | + | input <- ports.PointerMotion{X: 101, Y: 1, DX: -50} | |
| 630 | + | for { | |
| 631 | + | if v, ok := command(t, commands).(ports.PointerMotionTo); ok { | |
| 632 | + | if v.X != float64(want.Rect.X+want.Rect.W-1-100-r.X) || v.DX != -50 { | |
| 633 | + | t.Fatalf("locked motion %+v", v) | |
| 634 | + | } | |
| 635 | + | break | |
| 636 | + | } | |
| 637 | + | } | |
| 638 | + | client <- ports.PointerConstrained{} | |
| 639 | + | scene(t, scenes) | |
| 640 | + | if got := receive(t, constraints); got.Mode != ports.ConstraintNone { | |
| 641 | + | t.Fatalf("release %+v", got) | |
| 642 | + | } | |
| 643 | + | } | |
| @@ -570,3 +570,74 @@ func TestOutputScale(t *testing.T) { | |||||
| 570 | t.Fatal(s.Scale, s.OutputWidth) | 570 | t.Fatal(s.Scale, s.OutputWidth) | ||
| 571 | } | 571 | } | ||
| 572 | } | 572 | } | ||
| 573 | + | ||||
| 574 | + | func TestPointerConstraint(t *testing.T) { | |||
| 575 | + | cfg := config.Defaults() | |||
| 576 | + | cfg.Border.Width = 0 | |||
| 577 | + | cfg.Layout.Gaps = 0 | |||
| 578 | + | client := make(chan ports.ClientEvent, 8) | |||
| 579 | + | input := make(chan ports.InputEvent, 8) | |||
| 580 | + | output := make(chan ports.OutputEvent, 8) | |||
| 581 | + | commands := make(chan ports.ClientCommand, 64) | |||
| 582 | + | scenes := make(chan []ports.Scene, 1) | |||
| 583 | + | constraints := make(chan ports.PointerConstraint, 1) | |||
| 584 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Constraints: constraints}) | |||
| 585 | + | if err != nil { | |||
| 586 | + | t.Fatal(err) | |||
| 587 | + | } | |||
| 588 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 589 | + | defer cancel() | |||
| 590 | + | go c.Run(ctx) | |||
| 591 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "A", Width: 100, Height: 80}} | |||
| 592 | + | scene(t, scenes) | |||
| 593 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "B", Width: 100, Height: 80}} | |||
| 594 | + | scene(t, scenes) | |||
| 595 | + | input <- ports.PointerMotion{X: 150, Y: 40} | |||
| 596 | + | scene(t, scenes) // the pointer moves the focus to B | |||
| 597 | + | client <- ports.WindowMapped{ID: 1} | |||
| 598 | + | r := receive(t, scenes)[1].Windows[0].Rect | |||
| 599 | + | // The confine region is window-local and clipped to the window. | |||
| 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) | |||
| 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 | + | // 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 { | |||
| 605 | + | t.Fatalf("got %+v, want %+v", got, want) | |||
| 606 | + | } | |||
| 607 | + | for len(commands) > 0 { | |||
| 608 | + | <-commands | |||
| 609 | + | } | |||
| 610 | + | // Motion is clamped into the region; deltas pass through. | |||
| 611 | + | input <- ports.PointerMotion{X: 199, Y: 79, DX: 5, DY: 6} | |||
| 612 | + | for { | |||
| 613 | + | if v, ok := command(t, commands).(ports.PointerMotionTo); ok { | |||
| 614 | + | if v.X != float64(want.Rect.X+want.Rect.W-1-100-r.X) || v.Y != float64(want.Rect.Y+want.Rect.H-1-r.Y) || v.DX != 5 || v.DY != 6 { | |||
| 615 | + | t.Fatalf("motion %+v", v) | |||
| 616 | + | } | |||
| 617 | + | break | |||
| 618 | + | } | |||
| 619 | + | } | |||
| 620 | + | // A lock keeps the pointer still. | |||
| 621 | + | client <- ports.PointerConstrained{ID: 1, PointerConstraint: ports.PointerConstraint{Mode: ports.ConstraintLock}} | |||
| 622 | + | scene(t, scenes) | |||
| 623 | + | if got := receive(t, constraints); got.Mode != ports.ConstraintLock { | |||
| 624 | + | t.Fatalf("lock %+v", got) | |||
| 625 | + | } | |||
| 626 | + | for len(commands) > 0 { | |||
| 627 | + | <-commands | |||
| 628 | + | } | |||
| 629 | + | input <- ports.PointerMotion{X: 101, Y: 1, DX: -50} | |||
| 630 | + | for { | |||
| 631 | + | if v, ok := command(t, commands).(ports.PointerMotionTo); ok { | |||
| 632 | + | if v.X != float64(want.Rect.X+want.Rect.W-1-100-r.X) || v.DX != -50 { | |||
| 633 | + | t.Fatalf("locked motion %+v", v) | |||
| 634 | + | } | |||
| 635 | + | break | |||
| 636 | + | } | |||
| 637 | + | } | |||
| 638 | + | client <- ports.PointerConstrained{} | |||
| 639 | + | scene(t, scenes) | |||
| 640 | + | if got := receive(t, constraints); got.Mode != ports.ConstraintNone { | |||
| 641 | + | t.Fatalf("release %+v", got) | |||
| 642 | + | } | |||
| 643 | + | } | |||
M internal/ports/messages.go +60 -5
| @@ -83,6 +83,49 @@ type LayerChanged struct{ Layers []LayerSurface } | |||
| 83 | 83 | ||
| 84 | 84 | func (LayerChanged) clientEvent() {} | |
| 85 | 85 | ||
| 86 | + | // ConstraintMode is how a pointer constraint holds the pointer. | |
| 87 | + | type ConstraintMode uint8 | |
| 88 | + | ||
| 89 | + | const ( | |
| 90 | + | ConstraintNone ConstraintMode = iota | |
| 91 | + | // ConstraintLock keeps the pointer still: only relative motion flows. | |
| 92 | + | ConstraintLock | |
| 93 | + | // ConstraintConfine keeps the pointer inside Rect. | |
| 94 | + | ConstraintConfine | |
| 95 | + | ) | |
| 96 | + | ||
| 97 | + | // PointerConstraint is an active pointer constraint. Rect is logical: in | |
| 98 | + | // PointerConstrained it is window-local and empty means the whole window; | |
| 99 | + | // core sends input the resolved global rectangle, and in X, Y its cursor | |
| 100 | + | // position, where input holds a locked pointer. | |
| 101 | + | type PointerConstraint struct { | |
| 102 | + | Mode ConstraintMode | |
| 103 | + | Rect Rect | |
| 104 | + | X, Y float64 | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | // Clamp keeps a global point inside the rectangle of a lock or confine; | |
| 108 | + | // no constraint and an empty rectangle leave it unchanged. | |
| 109 | + | func (c PointerConstraint) Clamp(x, y float64) (float64, float64) { | |
| 110 | + | if c.Mode == ConstraintNone || c.Rect.W <= 0 || c.Rect.H <= 0 { | |
| 111 | + | return x, y | |
| 112 | + | } | |
| 113 | + | x = min(max(x, float64(c.Rect.X)), float64(c.Rect.X+c.Rect.W-1)) | |
| 114 | + | y = min(max(y, float64(c.Rect.Y)), float64(c.Rect.Y+c.Rect.H-1)) | |
| 115 | + | return x, y | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | // PointerConstrained carries wayland → core the constraint active on a | |
| 119 | + | // window (zwp_pointer_constraints_v1); ID 0 means none is active. A | |
| 120 | + | // constraint is active only on the window with both pointer and keyboard | |
| 121 | + | // focus. | |
| 122 | + | type PointerConstrained struct { | |
| 123 | + | ID WindowID | |
| 124 | + | PointerConstraint | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | func (PointerConstrained) clientEvent() {} | |
| 128 | + | ||
| 86 | 129 | // Mods carries input → core modifier flags. | |
| 87 | 130 | type Mods uint8 | |
| 88 | 131 | ||
| @@ -117,9 +160,15 @@ type ModState struct{ Depressed, Latched, Locked, Group uint32 } | |||
| 117 | 160 | func (KeyEvent) inputEvent() {} | |
| 118 | 161 | ||
| 119 | 162 | // PointerMotion uses global layout coordinates in logical pixels (see OutputPlacement). | |
| 163 | + | // DX and DY are the accelerated logical deltas and UnaccelDX, UnaccelDY the | |
| 164 | + | // raw device deltas; all four are 0 for absolute devices. TimeUsec is the | |
| 165 | + | // device timestamp in microseconds. | |
| 120 | 166 | type PointerMotion struct { | |
| 121 | - | X, Y float64 | |
| 122 | - | TimeMsec uint32 | |
| 167 | + | X, Y float64 | |
| 168 | + | DX, DY float64 | |
| 169 | + | UnaccelDX, UnaccelDY float64 | |
| 170 | + | TimeMsec uint32 | |
| 171 | + | TimeUsec uint64 | |
| 123 | 172 | } | |
| 124 | 173 | ||
| 125 | 174 | func (PointerMotion) inputEvent() {} | |
| @@ -263,10 +312,16 @@ type PointerFocus struct { | |||
| 263 | 312 | ||
| 264 | 313 | func (PointerFocus) clientCommand() {} | |
| 265 | 314 | ||
| 315 | + | // PointerMotionTo carries core → wayland motion for the pointer focus, in | |
| 316 | + | // window coordinates. The deltas and TimeUsec feed zwp_relative_pointer_v1; | |
| 317 | + | // While the window locks the pointer, wayland sends only relative motion. | |
| 266 | 318 | type PointerMotionTo struct { | |
| 267 | - | ID WindowID | |
| 268 | - | X, Y float64 | |
| 269 | - | TimeMsec uint32 | |
| 319 | + | ID WindowID | |
| 320 | + | X, Y float64 | |
| 321 | + | DX, DY float64 | |
| 322 | + | UnaccelDX, UnaccelDY float64 | |
| 323 | + | TimeMsec uint32 | |
| 324 | + | TimeUsec uint64 | |
| 270 | 325 | } | |
| 271 | 326 | ||
| 272 | 327 | func (PointerMotionTo) clientCommand() {} | |
| @@ -83,6 +83,49 @@ type LayerChanged struct{ Layers []LayerSurface } | |||||
| 83 | 83 | ||||
| 84 | func (LayerChanged) clientEvent() {} | 84 | func (LayerChanged) clientEvent() {} | ||
| 85 | 85 | ||||
| 86 | + | // ConstraintMode is how a pointer constraint holds the pointer. | |||
| 87 | + | type ConstraintMode uint8 | |||
| 88 | + | ||||
| 89 | + | const ( | |||
| 90 | + | ConstraintNone ConstraintMode = iota | |||
| 91 | + | // ConstraintLock keeps the pointer still: only relative motion flows. | |||
| 92 | + | ConstraintLock | |||
| 93 | + | // ConstraintConfine keeps the pointer inside Rect. | |||
| 94 | + | ConstraintConfine | |||
| 95 | + | ) | |||
| 96 | + | ||||
| 97 | + | // PointerConstraint is an active pointer constraint. Rect is logical: in | |||
| 98 | + | // PointerConstrained it is window-local and empty means the whole window; | |||
| 99 | + | // core sends input the resolved global rectangle, and in X, Y its cursor | |||
| 100 | + | // position, where input holds a locked pointer. | |||
| 101 | + | type PointerConstraint struct { | |||
| 102 | + | Mode ConstraintMode | |||
| 103 | + | Rect Rect | |||
| 104 | + | X, Y float64 | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | // Clamp keeps a global point inside the rectangle of a lock or confine; | |||
| 108 | + | // no constraint and an empty rectangle leave it unchanged. | |||
| 109 | + | func (c PointerConstraint) Clamp(x, y float64) (float64, float64) { | |||
| 110 | + | if c.Mode == ConstraintNone || c.Rect.W <= 0 || c.Rect.H <= 0 { | |||
| 111 | + | return x, y | |||
| 112 | + | } | |||
| 113 | + | x = min(max(x, float64(c.Rect.X)), float64(c.Rect.X+c.Rect.W-1)) | |||
| 114 | + | y = min(max(y, float64(c.Rect.Y)), float64(c.Rect.Y+c.Rect.H-1)) | |||
| 115 | + | return x, y | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | // PointerConstrained carries wayland → core the constraint active on a | |||
| 119 | + | // window (zwp_pointer_constraints_v1); ID 0 means none is active. A | |||
| 120 | + | // constraint is active only on the window with both pointer and keyboard | |||
| 121 | + | // focus. | |||
| 122 | + | type PointerConstrained struct { | |||
| 123 | + | ID WindowID | |||
| 124 | + | PointerConstraint | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | func (PointerConstrained) clientEvent() {} | |||
| 128 | + | ||||
| 86 | // Mods carries input → core modifier flags. | 129 | // Mods carries input → core modifier flags. | ||
| 87 | type Mods uint8 | 130 | type Mods uint8 | ||
| 88 | 131 | ||||
| @@ -117,9 +160,15 @@ type ModState struct{ Depressed, Latched, Locked, Group uint32 } | |||||
| 117 | func (KeyEvent) inputEvent() {} | 160 | func (KeyEvent) inputEvent() {} | ||
| 118 | 161 | ||||
| 119 | // PointerMotion uses global layout coordinates in logical pixels (see OutputPlacement). | 162 | // PointerMotion uses global layout coordinates in logical pixels (see OutputPlacement). | ||
| 163 | + | // DX and DY are the accelerated logical deltas and UnaccelDX, UnaccelDY the | |||
| 164 | + | // raw device deltas; all four are 0 for absolute devices. TimeUsec is the | |||
| 165 | + | // device timestamp in microseconds. | |||
| 120 | type PointerMotion struct { | 166 | type PointerMotion struct { | ||
| 121 | - | X, Y float64 | 167 | + | X, Y float64 |
| 122 | - | TimeMsec uint32 | 168 | + | DX, DY float64 |
| 169 | + | UnaccelDX, UnaccelDY float64 | |||
| 170 | + | TimeMsec uint32 | |||
| 171 | + | TimeUsec uint64 | |||
| 123 | } | 172 | } | ||
| 124 | 173 | ||||
| 125 | func (PointerMotion) inputEvent() {} | 174 | func (PointerMotion) inputEvent() {} | ||
| @@ -263,10 +312,16 @@ type PointerFocus struct { | |||||
| 263 | 312 | ||||
| 264 | func (PointerFocus) clientCommand() {} | 313 | func (PointerFocus) clientCommand() {} | ||
| 265 | 314 | ||||
| 315 | + | // PointerMotionTo carries core → wayland motion for the pointer focus, in | |||
| 316 | + | // window coordinates. The deltas and TimeUsec feed zwp_relative_pointer_v1; | |||
| 317 | + | // While the window locks the pointer, wayland sends only relative motion. | |||
| 266 | type PointerMotionTo struct { | 318 | type PointerMotionTo struct { | ||
| 267 | - | ID WindowID | 319 | + | ID WindowID |
| 268 | - | X, Y float64 | 320 | + | X, Y float64 |
| 269 | - | TimeMsec uint32 | 321 | + | DX, DY float64 |
| 322 | + | UnaccelDX, UnaccelDY float64 | |||
| 323 | + | TimeMsec uint32 | |||
| 324 | + | TimeUsec uint64 | |||
| 270 | } | 325 | } | ||
| 271 | 326 | ||||
| 272 | func (PointerMotionTo) clientCommand() {} | 327 | func (PointerMotionTo) clientCommand() {} | ||