feat(wayland): implement zwp_input_timestamps_v1 (#122)
* refactor(ports): carry input timestamps as time.Duration Replace the TimeMsec/TimeUsec pairs on key and pointer input types with a single Time (CLOCK_MONOTONIC), like the swipe events. The wayland adapter derives the wire millisecond values with wireMsec and the relative pointer microseconds from it, so wire output is unchanged. Core edge-scroll timing keeps its 250 ms interval as a duration. * perf(wayland): return seat device lists without allocating The per-client pointer and keyboard lists now hold only live resources: OnDestroy removes a device copy-on-write, so clientPointers and clientKeyboards return the stored slice instead of filtering into a new one on every input event. Add an allocation test for both getters. * feat(wayland): implement zwp_input_timestamps_v1 Advertise zwp_input_timestamps_manager_v1 version 1. A subscribed wl_keyboard or wl_pointer receives timestamp(sec_hi, sec_lo, nsec) right before each key, motion, button, axis and axis_stop event; unsubscribed devices and destroyed or inert objects get nothing. Subscriptions are kept copy-on-write per device resource and dropped with the device. The virtual keyboard forward and its release-on-destroy are stamped too; the input-method grab keeps its own interface.
- commit
- fc554045aaac4a0fd5bea58b4899dc621f90e2ac
- author
- bnema · · 2026-10-03 20:13 UTC
- committer
- GitHub · · 2026-10-03 20:13 UTC
- parent
- a59ec80
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
31 files · +523 -143
M README.md +1 -1
| @@ -61,7 +61,7 @@ The terminal command comes from `terminal`, then `$TERMINAL`, then `foot`. `term | |||
| 61 | 61 | - `wp_presentation`, `wp_fifo_v1`, `wp_commit_timing_v1` for frame pacing. | |
| 62 | 62 | - `wp_fractional_scale_v1` and `wp_viewporter`: clients render at the output scale, nothing is rescaled. | |
| 63 | 63 | - Damage tracking, occlusion of surfaces behind opaque ones, no work on idle outputs. Frame, cursor, overlay and VRR in one atomic commit. | |
| 64 | - | - Input is read on its own thread and never waits on rendering; the cursor has a hardware plane. With CAP_SYS_NICE, input and output threads request real-time scheduling. Relative pointer, pointer constraints and keyboard shortcuts inhibit for games. | |
| 64 | + | - Input is read on its own thread and never waits on rendering; the cursor has a hardware plane. With CAP_SYS_NICE, input and output threads request real-time scheduling. Relative pointer, pointer constraints and keyboard shortcuts inhibit for games. `zwp_input_timestamps_v1` gives clients microsecond input timestamps from libinput. | |
| 65 | 65 | - `wp_drm_lease_v1` leases headset connectors to SteamVR and Monado. | |
| 66 | 66 | ||
| 67 | 67 | ## Hardware support | |
| @@ -61,7 +61,7 @@ The terminal command comes from `terminal`, then `$TERMINAL`, then `foot`. `term | |||||
| 61 | - `wp_presentation`, `wp_fifo_v1`, `wp_commit_timing_v1` for frame pacing. | 61 | - `wp_presentation`, `wp_fifo_v1`, `wp_commit_timing_v1` for frame pacing. | ||
| 62 | - `wp_fractional_scale_v1` and `wp_viewporter`: clients render at the output scale, nothing is rescaled. | 62 | - `wp_fractional_scale_v1` and `wp_viewporter`: clients render at the output scale, nothing is rescaled. | ||
| 63 | - Damage tracking, occlusion of surfaces behind opaque ones, no work on idle outputs. Frame, cursor, overlay and VRR in one atomic commit. | 63 | - Damage tracking, occlusion of surfaces behind opaque ones, no work on idle outputs. Frame, cursor, overlay and VRR in one atomic commit. | ||
| 64 | - | - Input is read on its own thread and never waits on rendering; the cursor has a hardware plane. With CAP_SYS_NICE, input and output threads request real-time scheduling. Relative pointer, pointer constraints and keyboard shortcuts inhibit for games. | 64 | + | - Input is read on its own thread and never waits on rendering; the cursor has a hardware plane. With CAP_SYS_NICE, input and output threads request real-time scheduling. Relative pointer, pointer constraints and keyboard shortcuts inhibit for games. `zwp_input_timestamps_v1` gives clients microsecond input timestamps from libinput. |
| 65 | - `wp_drm_lease_v1` leases headset connectors to SteamVR and Monado. | 65 | - `wp_drm_lease_v1` leases headset connectors to SteamVR and Monado. | ||
| 66 | 66 | ||||
| 67 | ## Hardware support | 67 | ## Hardware support | ||
M internal/adapters/headlessinput/input.go +6 -6
| @@ -56,7 +56,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap, | |||
| 56 | 56 | if security != nil { | |
| 57 | 57 | var deliver bool | |
| 58 | 58 | var err error | |
| 59 | - | key, deliver, err = km.KeySecure(code, down, msec(monotonic()), state) | |
| 59 | + | key, deliver, err = km.KeySecure(code, down, monotonic(), state) | |
| 60 | 60 | if err != nil { | |
| 61 | 61 | return err | |
| 62 | 62 | } | |
| @@ -64,7 +64,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap, | |||
| 64 | 64 | return nil | |
| 65 | 65 | } | |
| 66 | 66 | } else { | |
| 67 | - | key = km.Key(code, down, msec(monotonic())) | |
| 67 | + | key = km.Key(code, down, monotonic()) | |
| 68 | 68 | } | |
| 69 | 69 | ev := stamp(key, state) | |
| 70 | 70 | select { | |
| @@ -154,7 +154,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap, | |||
| 154 | 154 | } | |
| 155 | 155 | return 0 | |
| 156 | 156 | } | |
| 157 | - | now := msec(monotonic()) | |
| 157 | + | now := monotonic() | |
| 158 | 158 | if len(fields) > 0 && fields[0] == "move" { | |
| 159 | 159 | if len(fields) == 3 { | |
| 160 | 160 | x, ex := strconv.ParseFloat(fields[1], 64) | |
| @@ -164,7 +164,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap, | |||
| 164 | 164 | tx, ty := o.ToTarget(x, y) | |
| 165 | 165 | moveCursor(o.Info.Name, tx, ty) | |
| 166 | 166 | } | |
| 167 | - | if err := sendPointer(ports.PointerMotion{X: x, Y: y, TimeMsec: now}); err != nil { | |
| 167 | + | if err := sendPointer(ports.PointerMotion{X: x, Y: y, Time: now}); err != nil { | |
| 168 | 168 | return err | |
| 169 | 169 | } | |
| 170 | 170 | continue | |
| @@ -205,12 +205,12 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap, | |||
| 205 | 205 | continue | |
| 206 | 206 | } | |
| 207 | 207 | if fields[0] != "up" { | |
| 208 | - | if err := sendPointer(ports.PointerButton{Button: code, Pressed: true, TimeMsec: now}); err != nil { | |
| 208 | + | if err := sendPointer(ports.PointerButton{Button: code, Pressed: true, Time: now}); err != nil { | |
| 209 | 209 | return err | |
| 210 | 210 | } | |
| 211 | 211 | } | |
| 212 | 212 | if fields[0] != "down" { | |
| 213 | - | if err := sendPointer(ports.PointerButton{Button: code, TimeMsec: now}); err != nil { | |
| 213 | + | if err := sendPointer(ports.PointerButton{Button: code, Time: now}); err != nil { | |
| 214 | 214 | return err | |
| 215 | 215 | } | |
| 216 | 216 | } | |
| @@ -56,7 +56,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap, | |||||
| 56 | if security != nil { | 56 | if security != nil { | ||
| 57 | var deliver bool | 57 | var deliver bool | ||
| 58 | var err error | 58 | var err error | ||
| 59 | - | key, deliver, err = km.KeySecure(code, down, msec(monotonic()), state) | 59 | + | key, deliver, err = km.KeySecure(code, down, monotonic(), state) |
| 60 | if err != nil { | 60 | if err != nil { | ||
| 61 | return err | 61 | return err | ||
| 62 | } | 62 | } | ||
| @@ -64,7 +64,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap, | |||||
| 64 | return nil | 64 | return nil | ||
| 65 | } | 65 | } | ||
| 66 | } else { | 66 | } else { | ||
| 67 | - | key = km.Key(code, down, msec(monotonic())) | 67 | + | key = km.Key(code, down, monotonic()) |
| 68 | } | 68 | } | ||
| 69 | ev := stamp(key, state) | 69 | ev := stamp(key, state) | ||
| 70 | select { | 70 | select { | ||
| @@ -154,7 +154,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap, | |||||
| 154 | } | 154 | } | ||
| 155 | return 0 | 155 | return 0 | ||
| 156 | } | 156 | } | ||
| 157 | - | now := msec(monotonic()) | 157 | + | now := monotonic() |
| 158 | if len(fields) > 0 && fields[0] == "move" { | 158 | if len(fields) > 0 && fields[0] == "move" { | ||
| 159 | if len(fields) == 3 { | 159 | if len(fields) == 3 { | ||
| 160 | x, ex := strconv.ParseFloat(fields[1], 64) | 160 | x, ex := strconv.ParseFloat(fields[1], 64) | ||
| @@ -164,7 +164,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap, | |||||
| 164 | tx, ty := o.ToTarget(x, y) | 164 | tx, ty := o.ToTarget(x, y) | ||
| 165 | moveCursor(o.Info.Name, tx, ty) | 165 | moveCursor(o.Info.Name, tx, ty) | ||
| 166 | } | 166 | } | ||
| 167 | - | if err := sendPointer(ports.PointerMotion{X: x, Y: y, TimeMsec: now}); err != nil { | 167 | + | if err := sendPointer(ports.PointerMotion{X: x, Y: y, Time: now}); err != nil { |
| 168 | return err | 168 | return err | ||
| 169 | } | 169 | } | ||
| 170 | continue | 170 | continue | ||
| @@ -205,12 +205,12 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap, | |||||
| 205 | continue | 205 | continue | ||
| 206 | } | 206 | } | ||
| 207 | if fields[0] != "up" { | 207 | if fields[0] != "up" { | ||
| 208 | - | if err := sendPointer(ports.PointerButton{Button: code, Pressed: true, TimeMsec: now}); err != nil { | 208 | + | if err := sendPointer(ports.PointerButton{Button: code, Pressed: true, Time: now}); err != nil { |
| 209 | return err | 209 | return err | ||
| 210 | } | 210 | } | ||
| 211 | } | 211 | } | ||
| 212 | if fields[0] != "down" { | 212 | if fields[0] != "down" { | ||
| 213 | - | if err := sendPointer(ports.PointerButton{Button: code, TimeMsec: now}); err != nil { | 213 | + | if err := sendPointer(ports.PointerButton{Button: code, Time: now}); err != nil { |
| 214 | return err | 214 | return err | ||
| 215 | } | 215 | } | ||
| 216 | } | 216 | } | ||
M internal/adapters/headlessinput/swipe.go +0 -2
| @@ -33,5 +33,3 @@ func monotonic() time.Duration { | |||
| 33 | 33 | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | |
| 34 | 34 | return time.Duration(ts.Nano()) | |
| 35 | 35 | } | |
| 36 | - | ||
| 37 | - | func msec(d time.Duration) uint32 { return uint32(d / time.Millisecond) } | |
| @@ -33,5 +33,3 @@ func monotonic() time.Duration { | |||||
| 33 | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | 33 | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | ||
| 34 | return time.Duration(ts.Nano()) | 34 | return time.Duration(ts.Nano()) | ||
| 35 | } | 35 | } | ||
| 36 | - | ||||
| 37 | - | func msec(d time.Duration) uint32 { return uint32(d / time.Millisecond) } | |||
M internal/adapters/libinput/libinput.go +10 -14
| @@ -511,9 +511,6 @@ func (s *swipes) end(dev uintptr, cancelled bool, at time.Duration) ports.InputE | |||
| 511 | 511 | // usec is a libinput timestamp as a duration. | |
| 512 | 512 | func usec(v uint64) time.Duration { return time.Duration(v) * time.Microsecond } | |
| 513 | 513 | ||
| 514 | - | // msec is a libinput timestamp in wayland's millisecond clock. | |
| 515 | - | func msec(v uint64) uint32 { return uint32(v / 1000) } | |
| 516 | - | ||
| 517 | 514 | // translate converts one libinput event. Timestamps are the device's | |
| 518 | 515 | // (CLOCK_MONOTONIC), so clients measure speeds (kinetic scrolling) on | |
| 519 | 516 | // when events happened, not when they were read. | |
| @@ -569,13 +566,12 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state | |||
| 569 | 566 | case evKeyboardKey: | |
| 570 | 567 | k := keyboardEvent(ev) | |
| 571 | 568 | code, pressed := keyboardKey(k), keyboardState(k) == 1 | |
| 572 | - | return translateKeyboard(code, pressed, msec(keyboardUsec(k)), opts, state) | |
| 569 | + | return translateKeyboard(code, pressed, usec(keyboardUsec(k)), opts, state) | |
| 573 | 570 | case evPointerMotion: | |
| 574 | 571 | pe := pointerEvent(ev) | |
| 575 | 572 | m := p.move(pointerDX(pe), pointerDY(pe)) | |
| 576 | 573 | m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe) | |
| 577 | - | m.TimeUsec = pointerUsec(pe) | |
| 578 | - | m.TimeMsec = msec(m.TimeUsec) | |
| 574 | + | m.Time = usec(pointerUsec(pe)) | |
| 579 | 575 | p.moved(opts.MoveCursor, true) | |
| 580 | 576 | if opts.LogMotion { | |
| 581 | 577 | log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer") | |
| @@ -584,24 +580,24 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state | |||
| 584 | 580 | case evPointerAbs: | |
| 585 | 581 | // Absolute devices (tablets, VMs) map to the whole layout. | |
| 586 | 582 | pe := pointerEvent(ev) | |
| 587 | - | now := msec(pointerUsec(pe)) | |
| 583 | + | now := usec(pointerUsec(pe)) | |
| 588 | 584 | b := p.bounds() | |
| 589 | 585 | if p.constraint.Mode == ports.ConstraintLock { | |
| 590 | - | return ports.PointerMotion{X: p.x, Y: p.y, TimeMsec: now}, nil | |
| 586 | + | return ports.PointerMotion{X: p.x, Y: p.y, Time: now}, nil | |
| 591 | 587 | } | |
| 592 | 588 | x, y := p.set(float64(b.X)+pointerAbsX(pe, uint32(b.W)), float64(b.Y)+pointerAbsY(pe, uint32(b.H))) | |
| 593 | 589 | p.moved(opts.MoveCursor, true) | |
| 594 | - | return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil | |
| 590 | + | return ports.PointerMotion{X: x, Y: y, Time: now}, nil | |
| 595 | 591 | case evPointerButton: | |
| 596 | 592 | pe := pointerEvent(ev) | |
| 597 | 593 | b, pressed := pointerButton(pe), pointerBtnState(pe) == 1 | |
| 598 | 594 | log.Debug().Uint32("button", b).Bool("pressed", pressed).Msg("button") | |
| 599 | - | return ports.PointerButton{Button: b, Pressed: pressed, TimeMsec: msec(pointerUsec(pe))}, nil | |
| 595 | + | return ports.PointerButton{Button: b, Pressed: pressed, Time: usec(pointerUsec(pe))}, nil | |
| 600 | 596 | case evScrollWheel, evScrollFinger, evScrollCont: | |
| 601 | 597 | pe := pointerEvent(ev) | |
| 602 | 598 | // The three scroll events follow each other as the sources do. | |
| 603 | 599 | source := ports.AxisSource(eventType(ev) - evScrollWheel) | |
| 604 | - | a := ports.PointerAxis{Source: source, TimeMsec: msec(pointerUsec(pe))} | |
| 600 | + | a := ports.PointerAxis{Source: source, Time: usec(pointerUsec(pe))} | |
| 605 | 601 | // libinput axis 0 is vertical, 1 horizontal, as in wl_pointer. | |
| 606 | 602 | for i, s := range []*ports.ScrollAxis{&a.Vertical, &a.Horizontal} { | |
| 607 | 603 | if pointerHasAxis(pe, uint32(i)) == 0 { | |
| @@ -626,17 +622,17 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state | |||
| 626 | 622 | ||
| 627 | 623 | // translateKeyboard applies the production epoch before native translation. | |
| 628 | 624 | // Quarantined events never reach logging, reserved hotkeys or core. | |
| 629 | - | func translateKeyboard(code uint32, pressed bool, timeMsec uint32, opts Options, state ports.SecurityState) (ports.InputEvent, error) { | |
| 625 | + | func translateKeyboard(code uint32, pressed bool, t time.Duration, opts Options, state ports.SecurityState) (ports.InputEvent, error) { | |
| 630 | 626 | var ke ports.KeyEvent | |
| 631 | 627 | if opts.Security != nil { | |
| 632 | 628 | var deliver bool | |
| 633 | 629 | var err error | |
| 634 | - | ke, deliver, err = opts.Keymap.KeySecure(code, pressed, timeMsec, state) | |
| 630 | + | ke, deliver, err = opts.Keymap.KeySecure(code, pressed, t, state) | |
| 635 | 631 | if err != nil || !deliver { | |
| 636 | 632 | return nil, err | |
| 637 | 633 | } | |
| 638 | 634 | } else { | |
| 639 | - | ke = opts.Keymap.Key(code, pressed, timeMsec) | |
| 635 | + | ke = opts.Keymap.Key(code, pressed, t) | |
| 640 | 636 | } | |
| 641 | 637 | return translateKey(ke, opts, state) | |
| 642 | 638 | } | |
| @@ -511,9 +511,6 @@ func (s *swipes) end(dev uintptr, cancelled bool, at time.Duration) ports.InputE | |||||
| 511 | // usec is a libinput timestamp as a duration. | 511 | // usec is a libinput timestamp as a duration. | ||
| 512 | func usec(v uint64) time.Duration { return time.Duration(v) * time.Microsecond } | 512 | func usec(v uint64) time.Duration { return time.Duration(v) * time.Microsecond } | ||
| 513 | 513 | ||||
| 514 | - | // msec is a libinput timestamp in wayland's millisecond clock. | |||
| 515 | - | func msec(v uint64) uint32 { return uint32(v / 1000) } | |||
| 516 | - | ||||
| 517 | // translate converts one libinput event. Timestamps are the device's | 514 | // translate converts one libinput event. Timestamps are the device's | ||
| 518 | // (CLOCK_MONOTONIC), so clients measure speeds (kinetic scrolling) on | 515 | // (CLOCK_MONOTONIC), so clients measure speeds (kinetic scrolling) on | ||
| 519 | // when events happened, not when they were read. | 516 | // when events happened, not when they were read. | ||
| @@ -569,13 +566,12 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state | |||||
| 569 | case evKeyboardKey: | 566 | case evKeyboardKey: | ||
| 570 | k := keyboardEvent(ev) | 567 | k := keyboardEvent(ev) | ||
| 571 | code, pressed := keyboardKey(k), keyboardState(k) == 1 | 568 | code, pressed := keyboardKey(k), keyboardState(k) == 1 | ||
| 572 | - | return translateKeyboard(code, pressed, msec(keyboardUsec(k)), opts, state) | 569 | + | return translateKeyboard(code, pressed, usec(keyboardUsec(k)), opts, state) |
| 573 | case evPointerMotion: | 570 | case evPointerMotion: | ||
| 574 | pe := pointerEvent(ev) | 571 | pe := pointerEvent(ev) | ||
| 575 | m := p.move(pointerDX(pe), pointerDY(pe)) | 572 | m := p.move(pointerDX(pe), pointerDY(pe)) | ||
| 576 | m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe) | 573 | m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe) | ||
| 577 | - | m.TimeUsec = pointerUsec(pe) | 574 | + | m.Time = usec(pointerUsec(pe)) |
| 578 | - | m.TimeMsec = msec(m.TimeUsec) | |||
| 579 | p.moved(opts.MoveCursor, true) | 575 | p.moved(opts.MoveCursor, true) | ||
| 580 | if opts.LogMotion { | 576 | if opts.LogMotion { | ||
| 581 | log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer") | 577 | log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer") | ||
| @@ -584,24 +580,24 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state | |||||
| 584 | case evPointerAbs: | 580 | case evPointerAbs: | ||
| 585 | // Absolute devices (tablets, VMs) map to the whole layout. | 581 | // Absolute devices (tablets, VMs) map to the whole layout. | ||
| 586 | pe := pointerEvent(ev) | 582 | pe := pointerEvent(ev) | ||
| 587 | - | now := msec(pointerUsec(pe)) | 583 | + | now := usec(pointerUsec(pe)) |
| 588 | b := p.bounds() | 584 | b := p.bounds() | ||
| 589 | if p.constraint.Mode == ports.ConstraintLock { | 585 | if p.constraint.Mode == ports.ConstraintLock { | ||
| 590 | - | return ports.PointerMotion{X: p.x, Y: p.y, TimeMsec: now}, nil | 586 | + | return ports.PointerMotion{X: p.x, Y: p.y, Time: now}, nil |
| 591 | } | 587 | } | ||
| 592 | x, y := p.set(float64(b.X)+pointerAbsX(pe, uint32(b.W)), float64(b.Y)+pointerAbsY(pe, uint32(b.H))) | 588 | x, y := p.set(float64(b.X)+pointerAbsX(pe, uint32(b.W)), float64(b.Y)+pointerAbsY(pe, uint32(b.H))) | ||
| 593 | p.moved(opts.MoveCursor, true) | 589 | p.moved(opts.MoveCursor, true) | ||
| 594 | - | return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil | 590 | + | return ports.PointerMotion{X: x, Y: y, Time: now}, nil |
| 595 | case evPointerButton: | 591 | case evPointerButton: | ||
| 596 | pe := pointerEvent(ev) | 592 | pe := pointerEvent(ev) | ||
| 597 | b, pressed := pointerButton(pe), pointerBtnState(pe) == 1 | 593 | b, pressed := pointerButton(pe), pointerBtnState(pe) == 1 | ||
| 598 | log.Debug().Uint32("button", b).Bool("pressed", pressed).Msg("button") | 594 | log.Debug().Uint32("button", b).Bool("pressed", pressed).Msg("button") | ||
| 599 | - | return ports.PointerButton{Button: b, Pressed: pressed, TimeMsec: msec(pointerUsec(pe))}, nil | 595 | + | return ports.PointerButton{Button: b, Pressed: pressed, Time: usec(pointerUsec(pe))}, nil |
| 600 | case evScrollWheel, evScrollFinger, evScrollCont: | 596 | case evScrollWheel, evScrollFinger, evScrollCont: | ||
| 601 | pe := pointerEvent(ev) | 597 | pe := pointerEvent(ev) | ||
| 602 | // The three scroll events follow each other as the sources do. | 598 | // The three scroll events follow each other as the sources do. | ||
| 603 | source := ports.AxisSource(eventType(ev) - evScrollWheel) | 599 | source := ports.AxisSource(eventType(ev) - evScrollWheel) | ||
| 604 | - | a := ports.PointerAxis{Source: source, TimeMsec: msec(pointerUsec(pe))} | 600 | + | a := ports.PointerAxis{Source: source, Time: usec(pointerUsec(pe))} |
| 605 | // libinput axis 0 is vertical, 1 horizontal, as in wl_pointer. | 601 | // libinput axis 0 is vertical, 1 horizontal, as in wl_pointer. | ||
| 606 | for i, s := range []*ports.ScrollAxis{&a.Vertical, &a.Horizontal} { | 602 | for i, s := range []*ports.ScrollAxis{&a.Vertical, &a.Horizontal} { | ||
| 607 | if pointerHasAxis(pe, uint32(i)) == 0 { | 603 | if pointerHasAxis(pe, uint32(i)) == 0 { | ||
| @@ -626,17 +622,17 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state | |||||
| 626 | 622 | ||||
| 627 | // translateKeyboard applies the production epoch before native translation. | 623 | // translateKeyboard applies the production epoch before native translation. | ||
| 628 | // Quarantined events never reach logging, reserved hotkeys or core. | 624 | // Quarantined events never reach logging, reserved hotkeys or core. | ||
| 629 | - | func translateKeyboard(code uint32, pressed bool, timeMsec uint32, opts Options, state ports.SecurityState) (ports.InputEvent, error) { | 625 | + | func translateKeyboard(code uint32, pressed bool, t time.Duration, opts Options, state ports.SecurityState) (ports.InputEvent, error) { |
| 630 | var ke ports.KeyEvent | 626 | var ke ports.KeyEvent | ||
| 631 | if opts.Security != nil { | 627 | if opts.Security != nil { | ||
| 632 | var deliver bool | 628 | var deliver bool | ||
| 633 | var err error | 629 | var err error | ||
| 634 | - | ke, deliver, err = opts.Keymap.KeySecure(code, pressed, timeMsec, state) | 630 | + | ke, deliver, err = opts.Keymap.KeySecure(code, pressed, t, state) |
| 635 | if err != nil || !deliver { | 631 | if err != nil || !deliver { | ||
| 636 | return nil, err | 632 | return nil, err | ||
| 637 | } | 633 | } | ||
| 638 | } else { | 634 | } else { | ||
| 639 | - | ke = opts.Keymap.Key(code, pressed, timeMsec) | 635 | + | ke = opts.Keymap.Key(code, pressed, t) |
| 640 | } | 636 | } | ||
| 641 | return translateKey(ke, opts, state) | 637 | return translateKey(ke, opts, state) | ||
| 642 | } | 638 | } | ||
M internal/adapters/libinput/libinput_test.go +3 -3
| @@ -107,8 +107,8 @@ func TestAccelProfile(t *testing.T) { | |||
| 107 | 107 | } | |
| 108 | 108 | ||
| 109 | 109 | func TestTimestamps(t *testing.T) { | |
| 110 | - | if usec(1500) != 1500*time.Microsecond || msec(1_234_567) != 1234 { | |
| 111 | - | t.Fatal(usec(1500), msec(1_234_567)) | |
| 110 | + | if usec(1500) != 1500*time.Microsecond { | |
| 111 | + | t.Fatal(usec(1500)) | |
| 112 | 112 | } | |
| 113 | 113 | } | |
| 114 | 114 | ||
| @@ -301,7 +301,7 @@ func TestKeyboardProducerQuarantinesBeforeHotkeysAndLogging(t *testing.T) { | |||
| 301 | 301 | produce := func(code uint32, down bool) ports.InputEvent { | |
| 302 | 302 | t.Helper() | |
| 303 | 303 | state := securitySnapshot(opts.Security) | |
| 304 | - | ev, err := translateKeyboard(code, down, 123, opts, state) | |
| 304 | + | ev, err := translateKeyboard(code, down, 123*time.Millisecond, opts, state) | |
| 305 | 305 | if err != nil { | |
| 306 | 306 | t.Fatal(err) | |
| 307 | 307 | } | |
| @@ -107,8 +107,8 @@ func TestAccelProfile(t *testing.T) { | |||||
| 107 | } | 107 | } | ||
| 108 | 108 | ||||
| 109 | func TestTimestamps(t *testing.T) { | 109 | func TestTimestamps(t *testing.T) { | ||
| 110 | - | if usec(1500) != 1500*time.Microsecond || msec(1_234_567) != 1234 { | 110 | + | if usec(1500) != 1500*time.Microsecond { |
| 111 | - | t.Fatal(usec(1500), msec(1_234_567)) | 111 | + | t.Fatal(usec(1500)) |
| 112 | } | 112 | } | ||
| 113 | } | 113 | } | ||
| 114 | 114 | ||||
| @@ -301,7 +301,7 @@ func TestKeyboardProducerQuarantinesBeforeHotkeysAndLogging(t *testing.T) { | |||||
| 301 | produce := func(code uint32, down bool) ports.InputEvent { | 301 | produce := func(code uint32, down bool) ports.InputEvent { | ||
| 302 | t.Helper() | 302 | t.Helper() | ||
| 303 | state := securitySnapshot(opts.Security) | 303 | state := securitySnapshot(opts.Security) | ||
| 304 | - | ev, err := translateKeyboard(code, down, 123, opts, state) | 304 | + | ev, err := translateKeyboard(code, down, 123*time.Millisecond, opts, state) |
| 305 | if err != nil { | 305 | if err != nil { | ||
| 306 | t.Fatal(err) | 306 | t.Fatal(err) | ||
| 307 | } | 307 | } | ||
M internal/adapters/wayland/constraints.go +4 -1
| @@ -1,6 +1,8 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "time" | |
| 5 | + | ||
| 4 | 6 | "github.com/bnema/go-wayland-bindings/server/pointerconstraints" | |
| 5 | 7 | "github.com/bnema/go-wayland-bindings/server/pointerwarp" | |
| 6 | 8 | "github.com/bnema/go-wayland-bindings/server/relativepointer" | |
| @@ -82,9 +84,10 @@ func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) bool { | |||
| 82 | 84 | return false | |
| 83 | 85 | } | |
| 84 | 86 | sent := false | |
| 87 | + | usec := uint64(c.Time / time.Microsecond) | |
| 85 | 88 | for _, rel := range s.relatives[w.xdg.resource.Client()] { | |
| 86 | 89 | if rel.Resource.Alive() { | |
| 87 | - | 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)) | |
| 90 | + | rel.SendRelativeMotion(uint32(usec>>32), uint32(usec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY)) | |
| 88 | 91 | sent = true | |
| 89 | 92 | } | |
| 90 | 93 | } | |
| @@ -1,6 +1,8 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "time" | |||
| 5 | + | ||||
| 4 | "github.com/bnema/go-wayland-bindings/server/pointerconstraints" | 6 | "github.com/bnema/go-wayland-bindings/server/pointerconstraints" | ||
| 5 | "github.com/bnema/go-wayland-bindings/server/pointerwarp" | 7 | "github.com/bnema/go-wayland-bindings/server/pointerwarp" | ||
| 6 | "github.com/bnema/go-wayland-bindings/server/relativepointer" | 8 | "github.com/bnema/go-wayland-bindings/server/relativepointer" | ||
| @@ -82,9 +84,10 @@ func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) bool { | |||||
| 82 | return false | 84 | return false | ||
| 83 | } | 85 | } | ||
| 84 | sent := false | 86 | sent := false | ||
| 87 | + | usec := uint64(c.Time / time.Microsecond) | |||
| 85 | for _, rel := range s.relatives[w.xdg.resource.Client()] { | 88 | for _, rel := range s.relatives[w.xdg.resource.Client()] { | ||
| 86 | if rel.Resource.Alive() { | 89 | if rel.Resource.Alive() { | ||
| 87 | - | 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)) | 90 | + | rel.SendRelativeMotion(uint32(usec>>32), uint32(usec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY)) |
| 88 | sent = true | 91 | sent = true | ||
| 89 | } | 92 | } | ||
| 90 | } | 93 | } | ||
M internal/adapters/wayland/globals.go +11 -14
| @@ -2,6 +2,7 @@ package wayland | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "os" | |
| 5 | + | "slices" | |
| 5 | 6 | ||
| 6 | 7 | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 7 | 8 | "github.com/bnema/neferwl/internal/ports" | |
| @@ -80,6 +81,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||
| 80 | 81 | } | |
| 81 | 82 | }) | |
| 82 | 83 | }, | |
| 84 | + | func() error { return registerInputTimestamps(d, s) }, | |
| 83 | 85 | func() error { return registerClipboard(d, s) }, | |
| 84 | 86 | func() error { return registerFixes(d) }, | |
| 85 | 87 | func() error { return registerAlphaModifier(d, s) }, | |
| @@ -433,13 +435,11 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) { | |||
| 433 | 435 | s.seat.pointers[r.Client()] = append(s.seat.pointers[r.Client()], p) | |
| 434 | 436 | p.OnDestroy = func() { | |
| 435 | 437 | delete(s.seat.enters, p.Resource) | |
| 436 | - | list := s.seat.pointers[r.Client()] | |
| 437 | - | for i, item := range list { | |
| 438 | - | if item == p { | |
| 439 | - | list = append(list[:i], list[i+1:]...) | |
| 440 | - | break | |
| 441 | - | } | |
| 442 | - | } | |
| 438 | + | // Subscribed timestamps objects become inert with the device. | |
| 439 | + | delete(s.seat.pointerStamps, p.Resource) | |
| 440 | + | // Copy on write: callers may be ranging over the old list (see | |
| 441 | + | // clientPointers). | |
| 442 | + | list := slices.DeleteFunc(slices.Clone(s.seat.pointers[r.Client()]), func(x *wayland.Pointer) bool { return x == p }) | |
| 443 | 443 | if len(list) == 0 { | |
| 444 | 444 | delete(s.seat.pointers, r.Client()) | |
| 445 | 445 | } else { | |
| @@ -462,13 +462,10 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) { | |||
| 462 | 462 | } | |
| 463 | 463 | s := h.server | |
| 464 | 464 | k.OnDestroy = func() { | |
| 465 | - | list := s.seat.keyboards[r.Client()] | |
| 466 | - | for i, item := range list { | |
| 467 | - | if item == k { | |
| 468 | - | list = append(list[:i], list[i+1:]...) | |
| 469 | - | break | |
| 470 | - | } | |
| 471 | - | } | |
| 465 | + | delete(s.seat.keyStamps, k.Resource) | |
| 466 | + | // Copy on write: callers may be ranging over the old list (see | |
| 467 | + | // clientKeyboards). | |
| 468 | + | list := slices.DeleteFunc(slices.Clone(s.seat.keyboards[r.Client()]), func(x *wayland.Keyboard) bool { return x == k }) | |
| 472 | 469 | if len(list) == 0 { | |
| 473 | 470 | delete(s.seat.keyboards, r.Client()) | |
| 474 | 471 | } else { | |
| @@ -2,6 +2,7 @@ package wayland | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "os" | 4 | "os" | ||
| 5 | + | "slices" | |||
| 5 | 6 | ||||
| 6 | "github.com/bnema/go-wayland-bindings/server/wayland" | 7 | "github.com/bnema/go-wayland-bindings/server/wayland" | ||
| 7 | "github.com/bnema/neferwl/internal/ports" | 8 | "github.com/bnema/neferwl/internal/ports" | ||
| @@ -80,6 +81,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||||
| 80 | } | 81 | } | ||
| 81 | }) | 82 | }) | ||
| 82 | }, | 83 | }, | ||
| 84 | + | func() error { return registerInputTimestamps(d, s) }, | |||
| 83 | func() error { return registerClipboard(d, s) }, | 85 | func() error { return registerClipboard(d, s) }, | ||
| 84 | func() error { return registerFixes(d) }, | 86 | func() error { return registerFixes(d) }, | ||
| 85 | func() error { return registerAlphaModifier(d, s) }, | 87 | func() error { return registerAlphaModifier(d, s) }, | ||
| @@ -433,13 +435,11 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) { | |||||
| 433 | s.seat.pointers[r.Client()] = append(s.seat.pointers[r.Client()], p) | 435 | s.seat.pointers[r.Client()] = append(s.seat.pointers[r.Client()], p) | ||
| 434 | p.OnDestroy = func() { | 436 | p.OnDestroy = func() { | ||
| 435 | delete(s.seat.enters, p.Resource) | 437 | delete(s.seat.enters, p.Resource) | ||
| 436 | - | list := s.seat.pointers[r.Client()] | 438 | + | // Subscribed timestamps objects become inert with the device. |
| 437 | - | for i, item := range list { | 439 | + | delete(s.seat.pointerStamps, p.Resource) |
| 438 | - | if item == p { | 440 | + | // Copy on write: callers may be ranging over the old list (see |
| 439 | - | list = append(list[:i], list[i+1:]...) | 441 | + | // clientPointers). |
| 440 | - | break | 442 | + | list := slices.DeleteFunc(slices.Clone(s.seat.pointers[r.Client()]), func(x *wayland.Pointer) bool { return x == p }) |
| 441 | - | } | |||
| 442 | - | } | |||
| 443 | if len(list) == 0 { | 443 | if len(list) == 0 { | ||
| 444 | delete(s.seat.pointers, r.Client()) | 444 | delete(s.seat.pointers, r.Client()) | ||
| 445 | } else { | 445 | } else { | ||
| @@ -462,13 +462,10 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) { | |||||
| 462 | } | 462 | } | ||
| 463 | s := h.server | 463 | s := h.server | ||
| 464 | k.OnDestroy = func() { | 464 | k.OnDestroy = func() { | ||
| 465 | - | list := s.seat.keyboards[r.Client()] | 465 | + | delete(s.seat.keyStamps, k.Resource) |
| 466 | - | for i, item := range list { | 466 | + | // Copy on write: callers may be ranging over the old list (see |
| 467 | - | if item == k { | 467 | + | // clientKeyboards). |
| 468 | - | list = append(list[:i], list[i+1:]...) | 468 | + | list := slices.DeleteFunc(slices.Clone(s.seat.keyboards[r.Client()]), func(x *wayland.Keyboard) bool { return x == k }) |
| 469 | - | break | |||
| 470 | - | } | |||
| 471 | - | } | |||
| 472 | if len(list) == 0 { | 469 | if len(list) == 0 { | ||
| 473 | delete(s.seat.keyboards, r.Client()) | 470 | delete(s.seat.keyboards, r.Client()) | ||
| 474 | } else { | 471 | } else { | ||
A internal/adapters/wayland/inputtimestamps.go +101 -0
| @@ -0,0 +1,101 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "slices" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/go-wayland-bindings/server/inputtimestamps" | |
| 8 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 9 | + | "github.com/bnema/purego-libwayland/server" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // zwp_input_timestamps_v1: a client subscribes a wl_keyboard or wl_pointer | |
| 13 | + | // and gets the device's high-resolution timestamp (libinput microseconds, in | |
| 14 | + | // the protocol's nanosecond fields) right before each key, motion, | |
| 15 | + | // button, axis and axis_stop event it sends. Subscriptions are per device | |
| 16 | + | // resource and die with the device (the object becomes inert) or when the | |
| 17 | + | // client destroys the timestamps object. There are no touch events, so touch | |
| 18 | + | // subscriptions only create an inert object. | |
| 19 | + | // | |
| 20 | + | // The stamp lists are replaced, never mutated, like the seat device lists. | |
| 21 | + | ||
| 22 | + | func registerInputTimestamps(d *server.Display, s *Server) error { | |
| 23 | + | return inputtimestamps.NewZwpInputTimestampsManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |
| 24 | + | _, _ = inputtimestamps.NewZwpInputTimestampsManagerV1(c, int32(v), id, timestampsManager{s}) | |
| 25 | + | }) | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | type timestampsManager struct{ server *Server } | |
| 29 | + | ||
| 30 | + | func (timestampsManager) Destroy(*inputtimestamps.ZwpInputTimestampsManagerV1) {} | |
| 31 | + | ||
| 32 | + | func (m timestampsManager) GetKeyboardTimestamps(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, kb *wayland.Keyboard) { | |
| 33 | + | var dev *server.Resource | |
| 34 | + | if kb != nil { | |
| 35 | + | dev = kb.Resource | |
| 36 | + | } | |
| 37 | + | m.subscribe(r, id, dev, m.server.seat.keyStamps) | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | func (m timestampsManager) GetPointerTimestamps(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, p *wayland.Pointer) { | |
| 41 | + | var dev *server.Resource | |
| 42 | + | if p != nil { | |
| 43 | + | dev = p.Resource | |
| 44 | + | } | |
| 45 | + | m.subscribe(r, id, dev, m.server.seat.pointerStamps) | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // GetTouchTimestamps creates the object only: the seat has no touch events. | |
| 49 | + | func (timestampsManager) GetTouchTimestamps(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, _ *wayland.Touch) { | |
| 50 | + | _, _ = inputtimestamps.NewZwpInputTimestampsV1(r.Client(), r.Version(), id, timestampsHandler{}) | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // subscribe creates the timestamps object for dev and records it in stamps. | |
| 54 | + | // A missing or dead device leaves the object inert. | |
| 55 | + | func (m timestampsManager) subscribe(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, dev *server.Resource, stamps map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1) { | |
| 56 | + | ts, err := inputtimestamps.NewZwpInputTimestampsV1(r.Client(), r.Version(), id, timestampsHandler{}) | |
| 57 | + | if err != nil || dev == nil || !dev.Alive() { | |
| 58 | + | return | |
| 59 | + | } | |
| 60 | + | stamps[dev] = append(slices.Clone(stamps[dev]), ts) | |
| 61 | + | ts.OnDestroy = func() { | |
| 62 | + | // The device may have died first and dropped its entry. | |
| 63 | + | list, ok := stamps[dev] | |
| 64 | + | if !ok { | |
| 65 | + | return | |
| 66 | + | } | |
| 67 | + | list = slices.DeleteFunc(slices.Clone(list), func(x *inputtimestamps.ZwpInputTimestampsV1) bool { return x == ts }) | |
| 68 | + | if len(list) == 0 { | |
| 69 | + | delete(stamps, dev) | |
| 70 | + | } else { | |
| 71 | + | stamps[dev] = list | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | type timestampsHandler struct{} | |
| 77 | + | ||
| 78 | + | func (timestampsHandler) Destroy(*inputtimestamps.ZwpInputTimestampsV1) {} | |
| 79 | + | ||
| 80 | + | // stampKeyboard sends t to the subscribers of k; call it right before a | |
| 81 | + | // wl_keyboard.key. It allocates nothing. | |
| 82 | + | func (s *Server) stampKeyboard(k *wayland.Keyboard, t time.Duration) { | |
| 83 | + | sendStamps(s.seat.keyStamps[k.Resource], t) | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | // stampPointer sends t to the subscribers of p; call it right before a | |
| 87 | + | // wl_pointer motion, button, axis or axis_stop. | |
| 88 | + | func (s *Server) stampPointer(p *wayland.Pointer, t time.Duration) { | |
| 89 | + | sendStamps(s.seat.pointerStamps[p.Resource], t) | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | func sendStamps(list []*inputtimestamps.ZwpInputTimestampsV1, t time.Duration) { | |
| 93 | + | if len(list) == 0 { | |
| 94 | + | return | |
| 95 | + | } | |
| 96 | + | sec := uint64(t / time.Second) | |
| 97 | + | ns := uint32(t % time.Second) | |
| 98 | + | for _, ts := range list { | |
| 99 | + | ts.SendTimestamp(uint32(sec>>32), uint32(sec), ns) | |
| 100 | + | } | |
| 101 | + | } | |
| @@ -0,0 +1,101 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/go-wayland-bindings/server/inputtimestamps" | |||
| 8 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 9 | + | "github.com/bnema/purego-libwayland/server" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // zwp_input_timestamps_v1: a client subscribes a wl_keyboard or wl_pointer | |||
| 13 | + | // and gets the device's high-resolution timestamp (libinput microseconds, in | |||
| 14 | + | // the protocol's nanosecond fields) right before each key, motion, | |||
| 15 | + | // button, axis and axis_stop event it sends. Subscriptions are per device | |||
| 16 | + | // resource and die with the device (the object becomes inert) or when the | |||
| 17 | + | // client destroys the timestamps object. There are no touch events, so touch | |||
| 18 | + | // subscriptions only create an inert object. | |||
| 19 | + | // | |||
| 20 | + | // The stamp lists are replaced, never mutated, like the seat device lists. | |||
| 21 | + | ||||
| 22 | + | func registerInputTimestamps(d *server.Display, s *Server) error { | |||
| 23 | + | return inputtimestamps.NewZwpInputTimestampsManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |||
| 24 | + | _, _ = inputtimestamps.NewZwpInputTimestampsManagerV1(c, int32(v), id, timestampsManager{s}) | |||
| 25 | + | }) | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | type timestampsManager struct{ server *Server } | |||
| 29 | + | ||||
| 30 | + | func (timestampsManager) Destroy(*inputtimestamps.ZwpInputTimestampsManagerV1) {} | |||
| 31 | + | ||||
| 32 | + | func (m timestampsManager) GetKeyboardTimestamps(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, kb *wayland.Keyboard) { | |||
| 33 | + | var dev *server.Resource | |||
| 34 | + | if kb != nil { | |||
| 35 | + | dev = kb.Resource | |||
| 36 | + | } | |||
| 37 | + | m.subscribe(r, id, dev, m.server.seat.keyStamps) | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | func (m timestampsManager) GetPointerTimestamps(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, p *wayland.Pointer) { | |||
| 41 | + | var dev *server.Resource | |||
| 42 | + | if p != nil { | |||
| 43 | + | dev = p.Resource | |||
| 44 | + | } | |||
| 45 | + | m.subscribe(r, id, dev, m.server.seat.pointerStamps) | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | // GetTouchTimestamps creates the object only: the seat has no touch events. | |||
| 49 | + | func (timestampsManager) GetTouchTimestamps(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, _ *wayland.Touch) { | |||
| 50 | + | _, _ = inputtimestamps.NewZwpInputTimestampsV1(r.Client(), r.Version(), id, timestampsHandler{}) | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // subscribe creates the timestamps object for dev and records it in stamps. | |||
| 54 | + | // A missing or dead device leaves the object inert. | |||
| 55 | + | func (m timestampsManager) subscribe(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, dev *server.Resource, stamps map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1) { | |||
| 56 | + | ts, err := inputtimestamps.NewZwpInputTimestampsV1(r.Client(), r.Version(), id, timestampsHandler{}) | |||
| 57 | + | if err != nil || dev == nil || !dev.Alive() { | |||
| 58 | + | return | |||
| 59 | + | } | |||
| 60 | + | stamps[dev] = append(slices.Clone(stamps[dev]), ts) | |||
| 61 | + | ts.OnDestroy = func() { | |||
| 62 | + | // The device may have died first and dropped its entry. | |||
| 63 | + | list, ok := stamps[dev] | |||
| 64 | + | if !ok { | |||
| 65 | + | return | |||
| 66 | + | } | |||
| 67 | + | list = slices.DeleteFunc(slices.Clone(list), func(x *inputtimestamps.ZwpInputTimestampsV1) bool { return x == ts }) | |||
| 68 | + | if len(list) == 0 { | |||
| 69 | + | delete(stamps, dev) | |||
| 70 | + | } else { | |||
| 71 | + | stamps[dev] = list | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | type timestampsHandler struct{} | |||
| 77 | + | ||||
| 78 | + | func (timestampsHandler) Destroy(*inputtimestamps.ZwpInputTimestampsV1) {} | |||
| 79 | + | ||||
| 80 | + | // stampKeyboard sends t to the subscribers of k; call it right before a | |||
| 81 | + | // wl_keyboard.key. It allocates nothing. | |||
| 82 | + | func (s *Server) stampKeyboard(k *wayland.Keyboard, t time.Duration) { | |||
| 83 | + | sendStamps(s.seat.keyStamps[k.Resource], t) | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | // stampPointer sends t to the subscribers of p; call it right before a | |||
| 87 | + | // wl_pointer motion, button, axis or axis_stop. | |||
| 88 | + | func (s *Server) stampPointer(p *wayland.Pointer, t time.Duration) { | |||
| 89 | + | sendStamps(s.seat.pointerStamps[p.Resource], t) | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | func sendStamps(list []*inputtimestamps.ZwpInputTimestampsV1, t time.Duration) { | |||
| 93 | + | if len(list) == 0 { | |||
| 94 | + | return | |||
| 95 | + | } | |||
| 96 | + | sec := uint64(t / time.Second) | |||
| 97 | + | ns := uint32(t % time.Second) | |||
| 98 | + | for _, ts := range list { | |||
| 99 | + | ts.SendTimestamp(uint32(sec>>32), uint32(sec), ns) | |||
| 100 | + | } | |||
| 101 | + | } | |||
A internal/adapters/wayland/inputtimestamps_test.go +257 -0
| @@ -0,0 +1,257 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "slices" | |
| 6 | + | "sync" | |
| 7 | + | "testing" | |
| 8 | + | "time" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/go-wayland-bindings/server/inputtimestamps" | |
| 11 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 12 | + | "github.com/bnema/neferwl/internal/ports" | |
| 13 | + | "github.com/bnema/wlturbo" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | // stampRecorder is the ordered log shared by the proxies of one client, so a | |
| 17 | + | // test sees whether a timestamp comes right before its event. | |
| 18 | + | type stampRecorder struct { | |
| 19 | + | mu sync.Mutex | |
| 20 | + | lines []string | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | func (r *stampRecorder) add(format string, args ...any) { | |
| 24 | + | r.mu.Lock() | |
| 25 | + | defer r.mu.Unlock() | |
| 26 | + | r.lines = append(r.lines, fmt.Sprintf(format, args...)) | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | func (r *stampRecorder) snapshot() []string { | |
| 30 | + | r.mu.Lock() | |
| 31 | + | defer r.mu.Unlock() | |
| 32 | + | return slices.Clone(r.lines) | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // wait round-trips until the log holds n lines. | |
| 36 | + | func (r *stampRecorder) wait(t *testing.T, c *wlturbo.Display, n int) []string { | |
| 37 | + | t.Helper() | |
| 38 | + | deadline := time.Now().Add(2 * time.Second) | |
| 39 | + | for { | |
| 40 | + | if err := c.Roundtrip(); err != nil { | |
| 41 | + | t.Fatal(err) | |
| 42 | + | } | |
| 43 | + | if got := r.snapshot(); len(got) >= n { | |
| 44 | + | return got | |
| 45 | + | } | |
| 46 | + | if time.Now().After(deadline) { | |
| 47 | + | t.Fatalf("log has %v, want %d lines", r.snapshot(), n) | |
| 48 | + | } | |
| 49 | + | time.Sleep(5 * time.Millisecond) | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // stampedKeyboard logs the key events of one wl_keyboard under its tag. | |
| 54 | + | type stampedKeyboard struct { | |
| 55 | + | wlturbo.BaseProxy | |
| 56 | + | tag string | |
| 57 | + | log *stampRecorder | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | func (p *stampedKeyboard) Dispatch(e *wlturbo.Event) { | |
| 61 | + | if e.Opcode == uint16(wayland.KeyboardEventKey) { | |
| 62 | + | _ = e.Uint32() // serial | |
| 63 | + | p.log.add("%s key %d", p.tag, e.Uint32()) | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | // stampedPointer logs the motion, button and axis events of a wl_pointer. | |
| 68 | + | type stampedPointer struct { | |
| 69 | + | wlturbo.BaseProxy | |
| 70 | + | log *stampRecorder | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | func (p *stampedPointer) Dispatch(e *wlturbo.Event) { | |
| 74 | + | switch e.Opcode { | |
| 75 | + | case uint16(wayland.PointerEventMotion): | |
| 76 | + | p.log.add("motion %d", e.Uint32()) | |
| 77 | + | case uint16(wayland.PointerEventButton): | |
| 78 | + | _ = e.Uint32() // serial | |
| 79 | + | p.log.add("button %d", e.Uint32()) | |
| 80 | + | case uint16(wayland.PointerEventAxis): | |
| 81 | + | p.log.add("axis %d", e.Uint32()) | |
| 82 | + | case uint16(wayland.PointerEventAxisStop): | |
| 83 | + | p.log.add("axis_stop %d", e.Uint32()) | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | // stampProxy logs zwp_input_timestamps_v1.timestamp events. | |
| 88 | + | type stampProxy struct { | |
| 89 | + | wlturbo.BaseProxy | |
| 90 | + | log *stampRecorder | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | func (p *stampProxy) Dispatch(e *wlturbo.Event) { | |
| 94 | + | if e.Opcode == uint16(inputtimestamps.ZwpInputTimestampsV1EventTimestamp) { | |
| 95 | + | hi, lo, ns := e.Uint32(), e.Uint32(), e.Uint32() | |
| 96 | + | p.log.add("ts %d %d %d", hi, lo, ns) | |
| 97 | + | } | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | func stampSubscription(t *testing.T, c *wlturbo.Display, log *stampRecorder, manager uint32, request uint32, device uint32) uint32 { | |
| 101 | + | t.Helper() | |
| 102 | + | id := c.AllocateID() | |
| 103 | + | p := &stampProxy{log: log} | |
| 104 | + | p.SetID(id) | |
| 105 | + | registerWireProxy(c, p) | |
| 106 | + | requestProtocol(t, c, manager, request, id, device) | |
| 107 | + | return id | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | func TestInputTimestampsGlobal(t *testing.T) { | |
| 111 | + | s, _, _, dir := keyboardServer(t) | |
| 112 | + | c := protocolClient(t, s, dir) | |
| 113 | + | g, ok := c.Registry().FindGlobal("zwp_input_timestamps_manager_v1") | |
| 114 | + | if !ok || g.Version != 1 { | |
| 115 | + | t.Fatalf("zwp_input_timestamps_manager_v1: %+v, %v", g, ok) | |
| 116 | + | } | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | func TestInputTimestampsKeyboard(t *testing.T) { | |
| 120 | + | s, events, commands, dir := keyboardServer(t) | |
| 121 | + | c := protocolClient(t, s, dir) | |
| 122 | + | log := &stampRecorder{} | |
| 123 | + | seat := bindProtocol(t, c, "wl_seat") | |
| 124 | + | registerProtocol(t, c, seat) | |
| 125 | + | var keyboards [2]uint32 | |
| 126 | + | for i, tag := range []string{"subscribed", "plain"} { | |
| 127 | + | keyboards[i] = c.AllocateID() | |
| 128 | + | kp := &stampedKeyboard{tag: tag, log: log} | |
| 129 | + | kp.SetID(keyboards[i]) | |
| 130 | + | registerWireProxy(c, kp) | |
| 131 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, keyboards[i]) | |
| 132 | + | } | |
| 133 | + | manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1") | |
| 134 | + | ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetKeyboardTimestamps, keyboards[0]) | |
| 135 | + | w := toplevelMapper(t, c, events)() | |
| 136 | + | commands <- ports.FocusWindow{ID: w.ID} | |
| 137 | + | ||
| 138 | + | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 5*time.Second + 123456789}} | |
| 139 | + | got := log.wait(t, c, 3) | |
| 140 | + | want := []string{"ts 0 5 123456789", "subscribed key 5123", "plain key 5123"} | |
| 141 | + | if !slices.Equal(got, want) { | |
| 142 | + | t.Fatalf("log %v, want %v", got, want) | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | // Seconds past 32 bits go to sec_hi. | |
| 146 | + | big := time.Duration(1<<32+7) * time.Second | |
| 147 | + | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Time: big}} | |
| 148 | + | ms := uint64(big / time.Millisecond) | |
| 149 | + | got = log.wait(t, c, 6) | |
| 150 | + | want = append(want, "ts 1 7 0", fmt.Sprint("subscribed key ", uint32(ms)), fmt.Sprint("plain key ", uint32(ms))) | |
| 151 | + | if !slices.Equal(got, want) { | |
| 152 | + | t.Fatalf("log %v, want %v", got, want) | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | // A destroyed timestamps object sends nothing more. | |
| 156 | + | requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy) | |
| 157 | + | if err := c.Roundtrip(); err != nil { | |
| 158 | + | t.Fatal(err) | |
| 159 | + | } | |
| 160 | + | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 31, Pressed: true, Time: 9 * time.Second}} | |
| 161 | + | got = log.wait(t, c, 8) | |
| 162 | + | want = append(want, "subscribed key 9000", "plain key 9000") | |
| 163 | + | if !slices.Equal(got, want) { | |
| 164 | + | t.Fatalf("log %v, want %v", got, want) | |
| 165 | + | } | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | func TestInputTimestampsPointer(t *testing.T) { | |
| 169 | + | s, events, commands, dir := keyboardServer(t) | |
| 170 | + | c := protocolClient(t, s, dir) | |
| 171 | + | log := &stampRecorder{} | |
| 172 | + | seat := bindVersion(t, c, "wl_seat", 8) | |
| 173 | + | registerProtocol(t, c, seat) | |
| 174 | + | pointer := c.AllocateID() | |
| 175 | + | pp := &stampedPointer{log: log} | |
| 176 | + | pp.SetID(pointer) | |
| 177 | + | registerWireProxy(c, pp) | |
| 178 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |
| 179 | + | manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1") | |
| 180 | + | ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetPointerTimestamps, pointer) | |
| 181 | + | w := toplevelMapper(t, c, events)() | |
| 182 | + | commands <- ports.PointerFocus{ID: w.ID, X: 0, Y: 0} | |
| 183 | + | ||
| 184 | + | commands <- ports.PointerMotionTo{ID: w.ID, X: 0, Y: 0, Time: 2*time.Second + time.Millisecond} | |
| 185 | + | commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, Time: 3 * time.Second} | |
| 186 | + | commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, Value: 5}, Time: 4 * time.Second}} | |
| 187 | + | commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, Time: 5 * time.Second}} | |
| 188 | + | got := log.wait(t, c, 8) | |
| 189 | + | want := []string{ | |
| 190 | + | "ts 0 2 1000000", "motion 2001", | |
| 191 | + | "ts 0 3 0", "button 3000", | |
| 192 | + | "ts 0 4 0", "axis 4000", | |
| 193 | + | "ts 0 5 0", "axis_stop 5000", | |
| 194 | + | } | |
| 195 | + | if !slices.Equal(got, want) { | |
| 196 | + | t.Fatalf("log %v, want %v", got, want) | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | // The stamped input path allocates nothing, with a live subscriber. | |
| 200 | + | var allocs float64 | |
| 201 | + | s.display.Do(func() { | |
| 202 | + | allocs = testing.AllocsPerRun(100, func() { | |
| 203 | + | s.applyInput(ports.PointerMotionTo{ID: w.ID, Time: 6 * time.Second}) | |
| 204 | + | }) | |
| 205 | + | }) | |
| 206 | + | if allocs != 0 { | |
| 207 | + | t.Fatalf("stamped motion allocates %v", allocs) | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | // Releasing the pointer makes the object inert and drops its entry; | |
| 211 | + | // destroying it afterwards is still fine. | |
| 212 | + | requestProtocol(t, c, pointer, wayland.PointerRequestRelease) | |
| 213 | + | if err := c.Roundtrip(); err != nil { | |
| 214 | + | t.Fatal(err) | |
| 215 | + | } | |
| 216 | + | if n := stampEntries(s); n != 0 { | |
| 217 | + | t.Fatalf("%d stamp entries after the pointer died", n) | |
| 218 | + | } | |
| 219 | + | requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy) | |
| 220 | + | if err := c.Roundtrip(); err != nil { | |
| 221 | + | t.Fatal(err) | |
| 222 | + | } | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | // Destroying the timestamps object before its device drops the entry too. | |
| 226 | + | func TestInputTimestampsDestroyBeforeDevice(t *testing.T) { | |
| 227 | + | s, _, _, dir := keyboardServer(t) | |
| 228 | + | c := protocolClient(t, s, dir) | |
| 229 | + | log := &stampRecorder{} | |
| 230 | + | seat := bindProtocol(t, c, "wl_seat") | |
| 231 | + | registerProtocol(t, c, seat) | |
| 232 | + | pointer := c.AllocateID() | |
| 233 | + | registerProtocol(t, c, pointer) | |
| 234 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |
| 235 | + | manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1") | |
| 236 | + | ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetPointerTimestamps, pointer) | |
| 237 | + | if err := c.Roundtrip(); err != nil { | |
| 238 | + | t.Fatal(err) | |
| 239 | + | } | |
| 240 | + | if n := stampEntries(s); n != 1 { | |
| 241 | + | t.Fatalf("%d stamp entries, want 1", n) | |
| 242 | + | } | |
| 243 | + | requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy) | |
| 244 | + | if err := c.Roundtrip(); err != nil { | |
| 245 | + | t.Fatal(err) | |
| 246 | + | } | |
| 247 | + | if n := stampEntries(s); n != 0 { | |
| 248 | + | t.Fatalf("%d stamp entries after destroy", n) | |
| 249 | + | } | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | // stampEntries counts the devices with subscriptions, on the display goroutine. | |
| 253 | + | func stampEntries(s *Server) int { | |
| 254 | + | var n int | |
| 255 | + | s.display.Do(func() { n = len(s.seat.keyStamps) + len(s.seat.pointerStamps) }) | |
| 256 | + | return n | |
| 257 | + | } | |
| @@ -0,0 +1,257 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "slices" | |||
| 6 | + | "sync" | |||
| 7 | + | "testing" | |||
| 8 | + | "time" | |||
| 9 | + | ||||
| 10 | + | "github.com/bnema/go-wayland-bindings/server/inputtimestamps" | |||
| 11 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 12 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 13 | + | "github.com/bnema/wlturbo" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | // stampRecorder is the ordered log shared by the proxies of one client, so a | |||
| 17 | + | // test sees whether a timestamp comes right before its event. | |||
| 18 | + | type stampRecorder struct { | |||
| 19 | + | mu sync.Mutex | |||
| 20 | + | lines []string | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | func (r *stampRecorder) add(format string, args ...any) { | |||
| 24 | + | r.mu.Lock() | |||
| 25 | + | defer r.mu.Unlock() | |||
| 26 | + | r.lines = append(r.lines, fmt.Sprintf(format, args...)) | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | func (r *stampRecorder) snapshot() []string { | |||
| 30 | + | r.mu.Lock() | |||
| 31 | + | defer r.mu.Unlock() | |||
| 32 | + | return slices.Clone(r.lines) | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // wait round-trips until the log holds n lines. | |||
| 36 | + | func (r *stampRecorder) wait(t *testing.T, c *wlturbo.Display, n int) []string { | |||
| 37 | + | t.Helper() | |||
| 38 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 39 | + | for { | |||
| 40 | + | if err := c.Roundtrip(); err != nil { | |||
| 41 | + | t.Fatal(err) | |||
| 42 | + | } | |||
| 43 | + | if got := r.snapshot(); len(got) >= n { | |||
| 44 | + | return got | |||
| 45 | + | } | |||
| 46 | + | if time.Now().After(deadline) { | |||
| 47 | + | t.Fatalf("log has %v, want %d lines", r.snapshot(), n) | |||
| 48 | + | } | |||
| 49 | + | time.Sleep(5 * time.Millisecond) | |||
| 50 | + | } | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // stampedKeyboard logs the key events of one wl_keyboard under its tag. | |||
| 54 | + | type stampedKeyboard struct { | |||
| 55 | + | wlturbo.BaseProxy | |||
| 56 | + | tag string | |||
| 57 | + | log *stampRecorder | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | func (p *stampedKeyboard) Dispatch(e *wlturbo.Event) { | |||
| 61 | + | if e.Opcode == uint16(wayland.KeyboardEventKey) { | |||
| 62 | + | _ = e.Uint32() // serial | |||
| 63 | + | p.log.add("%s key %d", p.tag, e.Uint32()) | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | // stampedPointer logs the motion, button and axis events of a wl_pointer. | |||
| 68 | + | type stampedPointer struct { | |||
| 69 | + | wlturbo.BaseProxy | |||
| 70 | + | log *stampRecorder | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | func (p *stampedPointer) Dispatch(e *wlturbo.Event) { | |||
| 74 | + | switch e.Opcode { | |||
| 75 | + | case uint16(wayland.PointerEventMotion): | |||
| 76 | + | p.log.add("motion %d", e.Uint32()) | |||
| 77 | + | case uint16(wayland.PointerEventButton): | |||
| 78 | + | _ = e.Uint32() // serial | |||
| 79 | + | p.log.add("button %d", e.Uint32()) | |||
| 80 | + | case uint16(wayland.PointerEventAxis): | |||
| 81 | + | p.log.add("axis %d", e.Uint32()) | |||
| 82 | + | case uint16(wayland.PointerEventAxisStop): | |||
| 83 | + | p.log.add("axis_stop %d", e.Uint32()) | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | // stampProxy logs zwp_input_timestamps_v1.timestamp events. | |||
| 88 | + | type stampProxy struct { | |||
| 89 | + | wlturbo.BaseProxy | |||
| 90 | + | log *stampRecorder | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | func (p *stampProxy) Dispatch(e *wlturbo.Event) { | |||
| 94 | + | if e.Opcode == uint16(inputtimestamps.ZwpInputTimestampsV1EventTimestamp) { | |||
| 95 | + | hi, lo, ns := e.Uint32(), e.Uint32(), e.Uint32() | |||
| 96 | + | p.log.add("ts %d %d %d", hi, lo, ns) | |||
| 97 | + | } | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | func stampSubscription(t *testing.T, c *wlturbo.Display, log *stampRecorder, manager uint32, request uint32, device uint32) uint32 { | |||
| 101 | + | t.Helper() | |||
| 102 | + | id := c.AllocateID() | |||
| 103 | + | p := &stampProxy{log: log} | |||
| 104 | + | p.SetID(id) | |||
| 105 | + | registerWireProxy(c, p) | |||
| 106 | + | requestProtocol(t, c, manager, request, id, device) | |||
| 107 | + | return id | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | func TestInputTimestampsGlobal(t *testing.T) { | |||
| 111 | + | s, _, _, dir := keyboardServer(t) | |||
| 112 | + | c := protocolClient(t, s, dir) | |||
| 113 | + | g, ok := c.Registry().FindGlobal("zwp_input_timestamps_manager_v1") | |||
| 114 | + | if !ok || g.Version != 1 { | |||
| 115 | + | t.Fatalf("zwp_input_timestamps_manager_v1: %+v, %v", g, ok) | |||
| 116 | + | } | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | func TestInputTimestampsKeyboard(t *testing.T) { | |||
| 120 | + | s, events, commands, dir := keyboardServer(t) | |||
| 121 | + | c := protocolClient(t, s, dir) | |||
| 122 | + | log := &stampRecorder{} | |||
| 123 | + | seat := bindProtocol(t, c, "wl_seat") | |||
| 124 | + | registerProtocol(t, c, seat) | |||
| 125 | + | var keyboards [2]uint32 | |||
| 126 | + | for i, tag := range []string{"subscribed", "plain"} { | |||
| 127 | + | keyboards[i] = c.AllocateID() | |||
| 128 | + | kp := &stampedKeyboard{tag: tag, log: log} | |||
| 129 | + | kp.SetID(keyboards[i]) | |||
| 130 | + | registerWireProxy(c, kp) | |||
| 131 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, keyboards[i]) | |||
| 132 | + | } | |||
| 133 | + | manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1") | |||
| 134 | + | ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetKeyboardTimestamps, keyboards[0]) | |||
| 135 | + | w := toplevelMapper(t, c, events)() | |||
| 136 | + | commands <- ports.FocusWindow{ID: w.ID} | |||
| 137 | + | ||||
| 138 | + | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 5*time.Second + 123456789}} | |||
| 139 | + | got := log.wait(t, c, 3) | |||
| 140 | + | want := []string{"ts 0 5 123456789", "subscribed key 5123", "plain key 5123"} | |||
| 141 | + | if !slices.Equal(got, want) { | |||
| 142 | + | t.Fatalf("log %v, want %v", got, want) | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | // Seconds past 32 bits go to sec_hi. | |||
| 146 | + | big := time.Duration(1<<32+7) * time.Second | |||
| 147 | + | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Time: big}} | |||
| 148 | + | ms := uint64(big / time.Millisecond) | |||
| 149 | + | got = log.wait(t, c, 6) | |||
| 150 | + | want = append(want, "ts 1 7 0", fmt.Sprint("subscribed key ", uint32(ms)), fmt.Sprint("plain key ", uint32(ms))) | |||
| 151 | + | if !slices.Equal(got, want) { | |||
| 152 | + | t.Fatalf("log %v, want %v", got, want) | |||
| 153 | + | } | |||
| 154 | + | ||||
| 155 | + | // A destroyed timestamps object sends nothing more. | |||
| 156 | + | requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy) | |||
| 157 | + | if err := c.Roundtrip(); err != nil { | |||
| 158 | + | t.Fatal(err) | |||
| 159 | + | } | |||
| 160 | + | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 31, Pressed: true, Time: 9 * time.Second}} | |||
| 161 | + | got = log.wait(t, c, 8) | |||
| 162 | + | want = append(want, "subscribed key 9000", "plain key 9000") | |||
| 163 | + | if !slices.Equal(got, want) { | |||
| 164 | + | t.Fatalf("log %v, want %v", got, want) | |||
| 165 | + | } | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | func TestInputTimestampsPointer(t *testing.T) { | |||
| 169 | + | s, events, commands, dir := keyboardServer(t) | |||
| 170 | + | c := protocolClient(t, s, dir) | |||
| 171 | + | log := &stampRecorder{} | |||
| 172 | + | seat := bindVersion(t, c, "wl_seat", 8) | |||
| 173 | + | registerProtocol(t, c, seat) | |||
| 174 | + | pointer := c.AllocateID() | |||
| 175 | + | pp := &stampedPointer{log: log} | |||
| 176 | + | pp.SetID(pointer) | |||
| 177 | + | registerWireProxy(c, pp) | |||
| 178 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 179 | + | manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1") | |||
| 180 | + | ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetPointerTimestamps, pointer) | |||
| 181 | + | w := toplevelMapper(t, c, events)() | |||
| 182 | + | commands <- ports.PointerFocus{ID: w.ID, X: 0, Y: 0} | |||
| 183 | + | ||||
| 184 | + | commands <- ports.PointerMotionTo{ID: w.ID, X: 0, Y: 0, Time: 2*time.Second + time.Millisecond} | |||
| 185 | + | commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, Time: 3 * time.Second} | |||
| 186 | + | commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, Value: 5}, Time: 4 * time.Second}} | |||
| 187 | + | commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, Time: 5 * time.Second}} | |||
| 188 | + | got := log.wait(t, c, 8) | |||
| 189 | + | want := []string{ | |||
| 190 | + | "ts 0 2 1000000", "motion 2001", | |||
| 191 | + | "ts 0 3 0", "button 3000", | |||
| 192 | + | "ts 0 4 0", "axis 4000", | |||
| 193 | + | "ts 0 5 0", "axis_stop 5000", | |||
| 194 | + | } | |||
| 195 | + | if !slices.Equal(got, want) { | |||
| 196 | + | t.Fatalf("log %v, want %v", got, want) | |||
| 197 | + | } | |||
| 198 | + | ||||
| 199 | + | // The stamped input path allocates nothing, with a live subscriber. | |||
| 200 | + | var allocs float64 | |||
| 201 | + | s.display.Do(func() { | |||
| 202 | + | allocs = testing.AllocsPerRun(100, func() { | |||
| 203 | + | s.applyInput(ports.PointerMotionTo{ID: w.ID, Time: 6 * time.Second}) | |||
| 204 | + | }) | |||
| 205 | + | }) | |||
| 206 | + | if allocs != 0 { | |||
| 207 | + | t.Fatalf("stamped motion allocates %v", allocs) | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | // Releasing the pointer makes the object inert and drops its entry; | |||
| 211 | + | // destroying it afterwards is still fine. | |||
| 212 | + | requestProtocol(t, c, pointer, wayland.PointerRequestRelease) | |||
| 213 | + | if err := c.Roundtrip(); err != nil { | |||
| 214 | + | t.Fatal(err) | |||
| 215 | + | } | |||
| 216 | + | if n := stampEntries(s); n != 0 { | |||
| 217 | + | t.Fatalf("%d stamp entries after the pointer died", n) | |||
| 218 | + | } | |||
| 219 | + | requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy) | |||
| 220 | + | if err := c.Roundtrip(); err != nil { | |||
| 221 | + | t.Fatal(err) | |||
| 222 | + | } | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | // Destroying the timestamps object before its device drops the entry too. | |||
| 226 | + | func TestInputTimestampsDestroyBeforeDevice(t *testing.T) { | |||
| 227 | + | s, _, _, dir := keyboardServer(t) | |||
| 228 | + | c := protocolClient(t, s, dir) | |||
| 229 | + | log := &stampRecorder{} | |||
| 230 | + | seat := bindProtocol(t, c, "wl_seat") | |||
| 231 | + | registerProtocol(t, c, seat) | |||
| 232 | + | pointer := c.AllocateID() | |||
| 233 | + | registerProtocol(t, c, pointer) | |||
| 234 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 235 | + | manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1") | |||
| 236 | + | ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetPointerTimestamps, pointer) | |||
| 237 | + | if err := c.Roundtrip(); err != nil { | |||
| 238 | + | t.Fatal(err) | |||
| 239 | + | } | |||
| 240 | + | if n := stampEntries(s); n != 1 { | |||
| 241 | + | t.Fatalf("%d stamp entries, want 1", n) | |||
| 242 | + | } | |||
| 243 | + | requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy) | |||
| 244 | + | if err := c.Roundtrip(); err != nil { | |||
| 245 | + | t.Fatal(err) | |||
| 246 | + | } | |||
| 247 | + | if n := stampEntries(s); n != 0 { | |||
| 248 | + | t.Fatalf("%d stamp entries after destroy", n) | |||
| 249 | + | } | |||
| 250 | + | } | |||
| 251 | + | ||||
| 252 | + | // stampEntries counts the devices with subscriptions, on the display goroutine. | |||
| 253 | + | func stampEntries(s *Server) int { | |||
| 254 | + | var n int | |||
| 255 | + | s.display.Do(func() { n = len(s.seat.keyStamps) + len(s.seat.pointerStamps) }) | |||
| 256 | + | return n | |||
| 257 | + | } | |||
M internal/adapters/wayland/keyboard_test.go +2 -2
| @@ -232,8 +232,8 @@ func TestFootKeyboard(t *testing.T) { | |||
| 232 | 232 | w := mapped(t, events, 10*time.Second) | |
| 233 | 233 | commands <- ports.FocusWindow{ID: w.ID} | |
| 234 | 234 | for _, code := range []uint32{18, 46, 35, 24, 57, 35, 23, 28} { | |
| 235 | - | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Pressed: true, TimeMsec: 1}} | |
| 236 | - | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, TimeMsec: 2}} | |
| 235 | + | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Pressed: true, Time: 1 * time.Millisecond}} | |
| 236 | + | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Time: 2 * time.Millisecond}} | |
| 237 | 237 | } | |
| 238 | 238 | enter := regexp.MustCompile(`wl_keyboard#[0-9]+\.enter\(`) | |
| 239 | 239 | key := regexp.MustCompile(`wl_keyboard#[0-9]+\.key\(`) | |
| @@ -232,8 +232,8 @@ func TestFootKeyboard(t *testing.T) { | |||||
| 232 | w := mapped(t, events, 10*time.Second) | 232 | w := mapped(t, events, 10*time.Second) | ||
| 233 | commands <- ports.FocusWindow{ID: w.ID} | 233 | commands <- ports.FocusWindow{ID: w.ID} | ||
| 234 | for _, code := range []uint32{18, 46, 35, 24, 57, 35, 23, 28} { | 234 | for _, code := range []uint32{18, 46, 35, 24, 57, 35, 23, 28} { | ||
| 235 | - | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Pressed: true, TimeMsec: 1}} | 235 | + | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Pressed: true, Time: 1 * time.Millisecond}} |
| 236 | - | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, TimeMsec: 2}} | 236 | + | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Time: 2 * time.Millisecond}} |
| 237 | } | 237 | } | ||
| 238 | enter := regexp.MustCompile(`wl_keyboard#[0-9]+\.enter\(`) | 238 | enter := regexp.MustCompile(`wl_keyboard#[0-9]+\.enter\(`) | ||
| 239 | key := regexp.MustCompile(`wl_keyboard#[0-9]+\.key\(`) | 239 | key := regexp.MustCompile(`wl_keyboard#[0-9]+\.key\(`) | ||
M internal/adapters/wayland/move_request_test.go +1 -1
| @@ -78,7 +78,7 @@ func TestToplevelMoveRequest(t *testing.T) { | |||
| 78 | 78 | } | |
| 79 | 79 | w := mapped(t, events, 2*time.Second) | |
| 80 | 80 | commands <- ports.PointerFocus{ID: w.ID, X: 1, Y: 1} | |
| 81 | - | commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, TimeMsec: 1} | |
| 81 | + | commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, Time: 1 * time.Millisecond} | |
| 82 | 82 | var serial uint32 | |
| 83 | 83 | deadline := time.After(2 * time.Second) | |
| 84 | 84 | for serial == 0 { | |
| @@ -78,7 +78,7 @@ func TestToplevelMoveRequest(t *testing.T) { | |||||
| 78 | } | 78 | } | ||
| 79 | w := mapped(t, events, 2*time.Second) | 79 | w := mapped(t, events, 2*time.Second) | ||
| 80 | commands <- ports.PointerFocus{ID: w.ID, X: 1, Y: 1} | 80 | commands <- ports.PointerFocus{ID: w.ID, X: 1, Y: 1} | ||
| 81 | - | commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, TimeMsec: 1} | 81 | + | commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, Time: 1 * time.Millisecond} |
| 82 | var serial uint32 | 82 | var serial uint32 | ||
| 83 | deadline := time.After(2 * time.Second) | 83 | deadline := time.After(2 * time.Second) | ||
| 84 | for serial == 0 { | 84 | for serial == 0 { | ||
M internal/adapters/wayland/protocol_test.go +6 -6
| @@ -510,13 +510,13 @@ func TestPointerProtocol(t *testing.T) { | |||
| 510 | 510 | } | |
| 511 | 511 | w := mapped(t, events, 2*time.Second) | |
| 512 | 512 | commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 20} | |
| 513 | - | commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, TimeMsec: 1} | |
| 513 | + | commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, Time: 1 * time.Millisecond} | |
| 514 | 514 | // Middle, side and extra buttons pass through as evdev codes. | |
| 515 | 515 | for _, b := range []uint32{0x112, 0x113, 0x114} { | |
| 516 | - | commands <- ports.PointerButtonTo{ID: w.ID, Button: b, Pressed: true, TimeMsec: 1} | |
| 516 | + | commands <- ports.PointerButtonTo{ID: w.ID, Button: b, Pressed: true, Time: 1 * time.Millisecond} | |
| 517 | 517 | } | |
| 518 | - | commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}, TimeMsec: 2}} | |
| 519 | - | commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, TimeMsec: 3}} | |
| 518 | + | commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}, Time: 2 * time.Millisecond}} | |
| 519 | + | commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, Time: 3 * time.Millisecond}} | |
| 520 | 520 | deadline := time.After(2 * time.Second) | |
| 521 | 521 | dispatched := make(chan error, 1) | |
| 522 | 522 | go func() { | |
| @@ -607,10 +607,10 @@ func TestHeldKeyOnKeyboardFocusTransfer(t *testing.T) { | |||
| 607 | 607 | } | |
| 608 | 608 | commands <- ports.FocusWindow{ID: a} | |
| 609 | 609 | enter(nil) | |
| 610 | - | commands <- ports.ForwardKey{ID: a, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 1}} | |
| 610 | + | commands <- ports.ForwardKey{ID: a, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 1 * time.Millisecond}} | |
| 611 | 611 | commands <- ports.FocusWindow{ID: b} | |
| 612 | 612 | enter([]byte{30, 0, 0, 0}) | |
| 613 | - | commands <- ports.ForwardKey{ID: b, Key: ports.KeyEvent{Keycode: 30, TimeMsec: 2}} | |
| 613 | + | commands <- ports.ForwardKey{ID: b, Key: ports.KeyEvent{Keycode: 30, Time: 2 * time.Millisecond}} | |
| 614 | 614 | commands <- ports.FocusWindow{ID: a} | |
| 615 | 615 | enter(nil) | |
| 616 | 616 | } | |
| @@ -510,13 +510,13 @@ func TestPointerProtocol(t *testing.T) { | |||||
| 510 | } | 510 | } | ||
| 511 | w := mapped(t, events, 2*time.Second) | 511 | w := mapped(t, events, 2*time.Second) | ||
| 512 | commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 20} | 512 | commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 20} | ||
| 513 | - | commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, TimeMsec: 1} | 513 | + | commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, Time: 1 * time.Millisecond} |
| 514 | // Middle, side and extra buttons pass through as evdev codes. | 514 | // Middle, side and extra buttons pass through as evdev codes. | ||
| 515 | for _, b := range []uint32{0x112, 0x113, 0x114} { | 515 | for _, b := range []uint32{0x112, 0x113, 0x114} { | ||
| 516 | - | commands <- ports.PointerButtonTo{ID: w.ID, Button: b, Pressed: true, TimeMsec: 1} | 516 | + | commands <- ports.PointerButtonTo{ID: w.ID, Button: b, Pressed: true, Time: 1 * time.Millisecond} |
| 517 | } | 517 | } | ||
| 518 | - | commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}, TimeMsec: 2}} | 518 | + | commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}, Time: 2 * time.Millisecond}} |
| 519 | - | commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, TimeMsec: 3}} | 519 | + | commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, Time: 3 * time.Millisecond}} |
| 520 | deadline := time.After(2 * time.Second) | 520 | deadline := time.After(2 * time.Second) | ||
| 521 | dispatched := make(chan error, 1) | 521 | dispatched := make(chan error, 1) | ||
| 522 | go func() { | 522 | go func() { | ||
| @@ -607,10 +607,10 @@ func TestHeldKeyOnKeyboardFocusTransfer(t *testing.T) { | |||||
| 607 | } | 607 | } | ||
| 608 | commands <- ports.FocusWindow{ID: a} | 608 | commands <- ports.FocusWindow{ID: a} | ||
| 609 | enter(nil) | 609 | enter(nil) | ||
| 610 | - | commands <- ports.ForwardKey{ID: a, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 1}} | 610 | + | commands <- ports.ForwardKey{ID: a, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 1 * time.Millisecond}} |
| 611 | commands <- ports.FocusWindow{ID: b} | 611 | commands <- ports.FocusWindow{ID: b} | ||
| 612 | enter([]byte{30, 0, 0, 0}) | 612 | enter([]byte{30, 0, 0, 0}) | ||
| 613 | - | commands <- ports.ForwardKey{ID: b, Key: ports.KeyEvent{Keycode: 30, TimeMsec: 2}} | 613 | + | commands <- ports.ForwardKey{ID: b, Key: ports.KeyEvent{Keycode: 30, Time: 2 * time.Millisecond}} |
| 614 | commands <- ports.FocusWindow{ID: a} | 614 | commands <- ports.FocusWindow{ID: a} | ||
| 615 | enter(nil) | 615 | enter(nil) | ||
| 616 | } | 616 | } | ||
M internal/adapters/wayland/seat.go +43 -29
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "fmt" | |
| 6 | 6 | "time" | |
| 7 | 7 | ||
| 8 | + | "github.com/bnema/go-wayland-bindings/server/inputtimestamps" | |
| 8 | 9 | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 9 | 10 | "github.com/bnema/neferwl/internal/ports" | |
| 10 | 11 | "github.com/bnema/purego-libwayland/server" | |
| @@ -43,6 +44,9 @@ type seatState struct { | |||
| 43 | 44 | // pressAt and focusAt date the last press and keyboard focus change, | |
| 44 | 45 | // for xdg-activation tokens. | |
| 45 | 46 | pressAt, focusAt time.Time | |
| 47 | + | // keyStamps and pointerStamps hold the zwp_input_timestamps_v1 objects | |
| 48 | + | // subscribed to each device; the lists are replaced, never mutated. | |
| 49 | + | keyStamps, pointerStamps map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1 | |
| 46 | 50 | } | |
| 47 | 51 | ||
| 48 | 52 | // applyInput delivers a core input command to the focused client. | |
| @@ -76,7 +80,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||
| 76 | 80 | if l := s.lockSurfaces[c.ID]; l != nil && s.lockInputTarget(c.ID) && c.ID == s.seat.pointerFocus { | |
| 77 | 81 | s.seat.pointerX, s.seat.pointerY = c.X, c.Y | |
| 78 | 82 | for _, p := range s.clientPointers(l.resource.Client()) { | |
| 79 | - | p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y)) | |
| 83 | + | s.stampPointer(p, c.Time) | |
| 84 | + | p.SendMotion(wireMsec(c.Time), server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y)) | |
| 80 | 85 | pointerFrame(p) | |
| 81 | 86 | } | |
| 82 | 87 | return | |
| @@ -84,7 +89,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||
| 84 | 89 | if l := s.layers[c.ID]; l != nil && c.ID == s.seat.pointerFocus { | |
| 85 | 90 | s.seat.pointerX, s.seat.pointerY = c.X, c.Y | |
| 86 | 91 | for _, p := range s.layerPointers(l) { | |
| 87 | - | p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y)) | |
| 92 | + | s.stampPointer(p, c.Time) | |
| 93 | + | p.SendMotion(wireMsec(c.Time), server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y)) | |
| 88 | 94 | pointerFrame(p) | |
| 89 | 95 | } | |
| 90 | 96 | return | |
| @@ -106,7 +112,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||
| 106 | 112 | s.updateConstraint() | |
| 107 | 113 | } | |
| 108 | 114 | for _, p := range s.windowPointers(w) { | |
| 109 | - | p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y)) | |
| 115 | + | s.stampPointer(p, c.Time) | |
| 116 | + | p.SendMotion(wireMsec(c.Time), server.FixedFromFloat(x), server.FixedFromFloat(y)) | |
| 110 | 117 | pointerFrame(p) | |
| 111 | 118 | } | |
| 112 | 119 | } | |
| @@ -120,7 +127,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||
| 120 | 127 | s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now() | |
| 121 | 128 | } | |
| 122 | 129 | for _, p := range pointers { | |
| 123 | - | p.SendButton(s.serial, c.TimeMsec, c.Button, state) | |
| 130 | + | s.stampPointer(p, c.Time) | |
| 131 | + | p.SendButton(s.serial, wireMsec(c.Time), c.Button, state) | |
| 124 | 132 | pointerFrame(p) | |
| 125 | 133 | } | |
| 126 | 134 | } | |
| @@ -132,7 +140,7 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||
| 132 | 140 | steps, values := s.seat.wheelSteps(c.Axis) | |
| 133 | 141 | _, pointers, _ := s.pointerTarget(c.ID) | |
| 134 | 142 | for _, p := range pointers { | |
| 135 | - | sendAxis(p, c.Axis, steps, values) | |
| 143 | + | s.sendAxis(p, c.Axis, steps, values) | |
| 136 | 144 | } | |
| 137 | 145 | case ports.SetKeymap: | |
| 138 | 146 | s.setKeymap(c) | |
| @@ -169,7 +177,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||
| 169 | 177 | } | |
| 170 | 178 | } | |
| 171 | 179 | for _, k := range keyboards { | |
| 172 | - | k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state) | |
| 180 | + | s.stampKeyboard(k, c.Key.Time) | |
| 181 | + | k.SendKey(s.serial, wireMsec(c.Key.Time), c.Key.Keycode, state) | |
| 173 | 182 | } | |
| 174 | 183 | if c.Key.State != s.seat.modState { | |
| 175 | 184 | s.seat.modState = c.Key.State | |
| @@ -188,14 +197,15 @@ func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | |||
| 188 | 197 | return s.clientKeyboards(w.xdg.resource.Client()) | |
| 189 | 198 | } | |
| 190 | 199 | ||
| 200 | + | // clientKeyboards returns c's wl_keyboards without allocating. The list holds | |
| 201 | + | // only live resources (OnDestroy removes a keyboard when it dies) and is | |
| 202 | + | // replaced, never mutated, so callers may iterate it while sending. | |
| 203 | + | // | |
| 204 | + | // This relies on purego-libwayland: destroyed() sets gone and runs OnDestroy | |
| 205 | + | // synchronously on the display goroutine, and PostEvent never re-enters Go, so | |
| 206 | + | // a list ranged over while sending is never changed under the caller. | |
| 191 | 207 | func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard { | |
| 192 | - | var alive []*wayland.Keyboard | |
| 193 | - | for _, k := range s.seat.keyboards[c] { | |
| 194 | - | if k.Resource.Alive() { | |
| 195 | - | alive = append(alive, k) | |
| 196 | - | } | |
| 197 | - | } | |
| 198 | - | return alive | |
| 208 | + | return s.seat.keyboards[c] | |
| 199 | 209 | } | |
| 200 | 210 | ||
| 201 | 211 | // focusTarget resolves a window or layer ID to its wl_surface and keyboards. | |
| @@ -395,7 +405,7 @@ func (st *seatState) wheelSteps(a ports.PointerAxis) (steps [2]int32, values [2] | |||
| 395 | 405 | // steps are whole wheel detents for axis_discrete. A pre-v8 client gets a | |
| 396 | 406 | // wheel axis only with a step, carrying values (all the value since the | |
| 397 | 407 | // last step), so it never counts smooth and discrete scroll. | |
| 398 | - | func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) { | |
| 408 | + | func (s *Server) sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) { | |
| 399 | 409 | v := p.Version() | |
| 400 | 410 | axes := [2]ports.ScrollAxis{a.Vertical, a.Horizontal} | |
| 401 | 411 | for i := range axes { | |
| @@ -409,21 +419,22 @@ func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2 | |||
| 409 | 419 | if v >= 5 { | |
| 410 | 420 | p.SendAxisSource(uint32(a.Source)) | |
| 411 | 421 | } | |
| 412 | - | for axis, s := range axes { | |
| 413 | - | if !s.Set { | |
| 422 | + | for axis, ax := range axes { | |
| 423 | + | if !ax.Set { | |
| 414 | 424 | continue | |
| 415 | 425 | } | |
| 416 | - | if s.Stop { | |
| 426 | + | if ax.Stop { | |
| 417 | 427 | if v >= 5 { | |
| 418 | - | p.SendAxisStop(a.TimeMsec, uint32(axis)) | |
| 428 | + | s.stampPointer(p, a.Time) | |
| 429 | + | p.SendAxisStop(wireMsec(a.Time), uint32(axis)) | |
| 419 | 430 | } | |
| 420 | 431 | continue | |
| 421 | 432 | } | |
| 422 | - | value := s.Value | |
| 423 | - | if a.Source == ports.AxisWheel && s.V120 != 0 { | |
| 433 | + | value := ax.Value | |
| 434 | + | if a.Source == ports.AxisWheel && ax.V120 != 0 { | |
| 424 | 435 | switch { | |
| 425 | 436 | case v >= 8: | |
| 426 | - | p.SendAxisValue120(uint32(axis), s.V120) | |
| 437 | + | p.SendAxisValue120(uint32(axis), ax.V120) | |
| 427 | 438 | default: | |
| 428 | 439 | value = values[axis] | |
| 429 | 440 | if v >= 5 { | |
| @@ -431,11 +442,16 @@ func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2 | |||
| 431 | 442 | } | |
| 432 | 443 | } | |
| 433 | 444 | } | |
| 434 | - | p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(value)) | |
| 445 | + | s.stampPointer(p, a.Time) | |
| 446 | + | p.SendAxis(wireMsec(a.Time), uint32(axis), server.FixedFromFloat(value)) | |
| 435 | 447 | } | |
| 436 | 448 | pointerFrame(p) | |
| 437 | 449 | } | |
| 438 | 450 | ||
| 451 | + | // wireMsec is a device timestamp as the 32-bit millisecond time of wl_pointer | |
| 452 | + | // and wl_keyboard events (wrapping, like the protocol). | |
| 453 | + | func wireMsec(t time.Duration) uint32 { return uint32(t / time.Millisecond) } | |
| 454 | + | ||
| 439 | 455 | func pointerFrame(p *wayland.Pointer) { | |
| 440 | 456 | if p.Version() >= 5 { | |
| 441 | 457 | p.SendFrame() | |
| @@ -455,14 +471,12 @@ func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer { | |||
| 455 | 471 | return s.clientPointers(l.resource.Client()) | |
| 456 | 472 | } | |
| 457 | 473 | ||
| 474 | + | // clientPointers returns c's wl_pointers without allocating. The list holds | |
| 475 | + | // only live resources (OnDestroy removes a pointer when it dies) and is | |
| 476 | + | // replaced, never mutated, so callers may iterate it while sending. See | |
| 477 | + | // clientKeyboards for the libwayland invariant this relies on. | |
| 458 | 478 | func (s *Server) clientPointers(c server.Client) []*wayland.Pointer { | |
| 459 | - | var result []*wayland.Pointer | |
| 460 | - | for _, p := range s.seat.pointers[c] { | |
| 461 | - | if p.Resource.Alive() { | |
| 462 | - | result = append(result, p) | |
| 463 | - | } | |
| 464 | - | } | |
| 465 | - | return result | |
| 479 | + | return s.seat.pointers[c] | |
| 466 | 480 | } | |
| 467 | 481 | func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) { | |
| 468 | 482 | if s.security != nil && s.security.Snapshot().Protected && id != 0 && !s.lockInputTarget(id) { | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "time" | 6 | "time" | ||
| 7 | 7 | ||||
| 8 | + | "github.com/bnema/go-wayland-bindings/server/inputtimestamps" | |||
| 8 | "github.com/bnema/go-wayland-bindings/server/wayland" | 9 | "github.com/bnema/go-wayland-bindings/server/wayland" | ||
| 9 | "github.com/bnema/neferwl/internal/ports" | 10 | "github.com/bnema/neferwl/internal/ports" | ||
| 10 | "github.com/bnema/purego-libwayland/server" | 11 | "github.com/bnema/purego-libwayland/server" | ||
| @@ -43,6 +44,9 @@ type seatState struct { | |||||
| 43 | // pressAt and focusAt date the last press and keyboard focus change, | 44 | // pressAt and focusAt date the last press and keyboard focus change, | ||
| 44 | // for xdg-activation tokens. | 45 | // for xdg-activation tokens. | ||
| 45 | pressAt, focusAt time.Time | 46 | pressAt, focusAt time.Time | ||
| 47 | + | // keyStamps and pointerStamps hold the zwp_input_timestamps_v1 objects | |||
| 48 | + | // subscribed to each device; the lists are replaced, never mutated. | |||
| 49 | + | keyStamps, pointerStamps map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1 | |||
| 46 | } | 50 | } | ||
| 47 | 51 | ||||
| 48 | // applyInput delivers a core input command to the focused client. | 52 | // applyInput delivers a core input command to the focused client. | ||
| @@ -76,7 +80,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||||
| 76 | if l := s.lockSurfaces[c.ID]; l != nil && s.lockInputTarget(c.ID) && c.ID == s.seat.pointerFocus { | 80 | if l := s.lockSurfaces[c.ID]; l != nil && s.lockInputTarget(c.ID) && c.ID == s.seat.pointerFocus { | ||
| 77 | s.seat.pointerX, s.seat.pointerY = c.X, c.Y | 81 | s.seat.pointerX, s.seat.pointerY = c.X, c.Y | ||
| 78 | for _, p := range s.clientPointers(l.resource.Client()) { | 82 | for _, p := range s.clientPointers(l.resource.Client()) { | ||
| 79 | - | p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y)) | 83 | + | s.stampPointer(p, c.Time) |
| 84 | + | p.SendMotion(wireMsec(c.Time), server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y)) | |||
| 80 | pointerFrame(p) | 85 | pointerFrame(p) | ||
| 81 | } | 86 | } | ||
| 82 | return | 87 | return | ||
| @@ -84,7 +89,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||||
| 84 | if l := s.layers[c.ID]; l != nil && c.ID == s.seat.pointerFocus { | 89 | if l := s.layers[c.ID]; l != nil && c.ID == s.seat.pointerFocus { | ||
| 85 | s.seat.pointerX, s.seat.pointerY = c.X, c.Y | 90 | s.seat.pointerX, s.seat.pointerY = c.X, c.Y | ||
| 86 | for _, p := range s.layerPointers(l) { | 91 | for _, p := range s.layerPointers(l) { | ||
| 87 | - | p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y)) | 92 | + | s.stampPointer(p, c.Time) |
| 93 | + | p.SendMotion(wireMsec(c.Time), server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y)) | |||
| 88 | pointerFrame(p) | 94 | pointerFrame(p) | ||
| 89 | } | 95 | } | ||
| 90 | return | 96 | return | ||
| @@ -106,7 +112,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||||
| 106 | s.updateConstraint() | 112 | s.updateConstraint() | ||
| 107 | } | 113 | } | ||
| 108 | for _, p := range s.windowPointers(w) { | 114 | for _, p := range s.windowPointers(w) { | ||
| 109 | - | p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y)) | 115 | + | s.stampPointer(p, c.Time) |
| 116 | + | p.SendMotion(wireMsec(c.Time), server.FixedFromFloat(x), server.FixedFromFloat(y)) | |||
| 110 | pointerFrame(p) | 117 | pointerFrame(p) | ||
| 111 | } | 118 | } | ||
| 112 | } | 119 | } | ||
| @@ -120,7 +127,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||||
| 120 | s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now() | 127 | s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now() | ||
| 121 | } | 128 | } | ||
| 122 | for _, p := range pointers { | 129 | for _, p := range pointers { | ||
| 123 | - | p.SendButton(s.serial, c.TimeMsec, c.Button, state) | 130 | + | s.stampPointer(p, c.Time) |
| 131 | + | p.SendButton(s.serial, wireMsec(c.Time), c.Button, state) | |||
| 124 | pointerFrame(p) | 132 | pointerFrame(p) | ||
| 125 | } | 133 | } | ||
| 126 | } | 134 | } | ||
| @@ -132,7 +140,7 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||||
| 132 | steps, values := s.seat.wheelSteps(c.Axis) | 140 | steps, values := s.seat.wheelSteps(c.Axis) | ||
| 133 | _, pointers, _ := s.pointerTarget(c.ID) | 141 | _, pointers, _ := s.pointerTarget(c.ID) | ||
| 134 | for _, p := range pointers { | 142 | for _, p := range pointers { | ||
| 135 | - | sendAxis(p, c.Axis, steps, values) | 143 | + | s.sendAxis(p, c.Axis, steps, values) |
| 136 | } | 144 | } | ||
| 137 | case ports.SetKeymap: | 145 | case ports.SetKeymap: | ||
| 138 | s.setKeymap(c) | 146 | s.setKeymap(c) | ||
| @@ -169,7 +177,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||||
| 169 | } | 177 | } | ||
| 170 | } | 178 | } | ||
| 171 | for _, k := range keyboards { | 179 | for _, k := range keyboards { | ||
| 172 | - | k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state) | 180 | + | s.stampKeyboard(k, c.Key.Time) |
| 181 | + | k.SendKey(s.serial, wireMsec(c.Key.Time), c.Key.Keycode, state) | |||
| 173 | } | 182 | } | ||
| 174 | if c.Key.State != s.seat.modState { | 183 | if c.Key.State != s.seat.modState { | ||
| 175 | s.seat.modState = c.Key.State | 184 | s.seat.modState = c.Key.State | ||
| @@ -188,14 +197,15 @@ func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | |||||
| 188 | return s.clientKeyboards(w.xdg.resource.Client()) | 197 | return s.clientKeyboards(w.xdg.resource.Client()) | ||
| 189 | } | 198 | } | ||
| 190 | 199 | ||||
| 200 | + | // clientKeyboards returns c's wl_keyboards without allocating. The list holds | |||
| 201 | + | // only live resources (OnDestroy removes a keyboard when it dies) and is | |||
| 202 | + | // replaced, never mutated, so callers may iterate it while sending. | |||
| 203 | + | // | |||
| 204 | + | // This relies on purego-libwayland: destroyed() sets gone and runs OnDestroy | |||
| 205 | + | // synchronously on the display goroutine, and PostEvent never re-enters Go, so | |||
| 206 | + | // a list ranged over while sending is never changed under the caller. | |||
| 191 | func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard { | 207 | func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard { | ||
| 192 | - | var alive []*wayland.Keyboard | 208 | + | return s.seat.keyboards[c] |
| 193 | - | for _, k := range s.seat.keyboards[c] { | |||
| 194 | - | if k.Resource.Alive() { | |||
| 195 | - | alive = append(alive, k) | |||
| 196 | - | } | |||
| 197 | - | } | |||
| 198 | - | return alive | |||
| 199 | } | 209 | } | ||
| 200 | 210 | ||||
| 201 | // focusTarget resolves a window or layer ID to its wl_surface and keyboards. | 211 | // focusTarget resolves a window or layer ID to its wl_surface and keyboards. | ||
| @@ -395,7 +405,7 @@ func (st *seatState) wheelSteps(a ports.PointerAxis) (steps [2]int32, values [2] | |||||
| 395 | // steps are whole wheel detents for axis_discrete. A pre-v8 client gets a | 405 | // steps are whole wheel detents for axis_discrete. A pre-v8 client gets a | ||
| 396 | // wheel axis only with a step, carrying values (all the value since the | 406 | // wheel axis only with a step, carrying values (all the value since the | ||
| 397 | // last step), so it never counts smooth and discrete scroll. | 407 | // last step), so it never counts smooth and discrete scroll. | ||
| 398 | - | func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) { | 408 | + | func (s *Server) sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) { |
| 399 | v := p.Version() | 409 | v := p.Version() | ||
| 400 | axes := [2]ports.ScrollAxis{a.Vertical, a.Horizontal} | 410 | axes := [2]ports.ScrollAxis{a.Vertical, a.Horizontal} | ||
| 401 | for i := range axes { | 411 | for i := range axes { | ||
| @@ -409,21 +419,22 @@ func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2 | |||||
| 409 | if v >= 5 { | 419 | if v >= 5 { | ||
| 410 | p.SendAxisSource(uint32(a.Source)) | 420 | p.SendAxisSource(uint32(a.Source)) | ||
| 411 | } | 421 | } | ||
| 412 | - | for axis, s := range axes { | 422 | + | for axis, ax := range axes { |
| 413 | - | if !s.Set { | 423 | + | if !ax.Set { |
| 414 | continue | 424 | continue | ||
| 415 | } | 425 | } | ||
| 416 | - | if s.Stop { | 426 | + | if ax.Stop { |
| 417 | if v >= 5 { | 427 | if v >= 5 { | ||
| 418 | - | p.SendAxisStop(a.TimeMsec, uint32(axis)) | 428 | + | s.stampPointer(p, a.Time) |
| 429 | + | p.SendAxisStop(wireMsec(a.Time), uint32(axis)) | |||
| 419 | } | 430 | } | ||
| 420 | continue | 431 | continue | ||
| 421 | } | 432 | } | ||
| 422 | - | value := s.Value | 433 | + | value := ax.Value |
| 423 | - | if a.Source == ports.AxisWheel && s.V120 != 0 { | 434 | + | if a.Source == ports.AxisWheel && ax.V120 != 0 { |
| 424 | switch { | 435 | switch { | ||
| 425 | case v >= 8: | 436 | case v >= 8: | ||
| 426 | - | p.SendAxisValue120(uint32(axis), s.V120) | 437 | + | p.SendAxisValue120(uint32(axis), ax.V120) |
| 427 | default: | 438 | default: | ||
| 428 | value = values[axis] | 439 | value = values[axis] | ||
| 429 | if v >= 5 { | 440 | if v >= 5 { | ||
| @@ -431,11 +442,16 @@ func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2 | |||||
| 431 | } | 442 | } | ||
| 432 | } | 443 | } | ||
| 433 | } | 444 | } | ||
| 434 | - | p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(value)) | 445 | + | s.stampPointer(p, a.Time) |
| 446 | + | p.SendAxis(wireMsec(a.Time), uint32(axis), server.FixedFromFloat(value)) | |||
| 435 | } | 447 | } | ||
| 436 | pointerFrame(p) | 448 | pointerFrame(p) | ||
| 437 | } | 449 | } | ||
| 438 | 450 | ||||
| 451 | + | // wireMsec is a device timestamp as the 32-bit millisecond time of wl_pointer | |||
| 452 | + | // and wl_keyboard events (wrapping, like the protocol). | |||
| 453 | + | func wireMsec(t time.Duration) uint32 { return uint32(t / time.Millisecond) } | |||
| 454 | + | ||||
| 439 | func pointerFrame(p *wayland.Pointer) { | 455 | func pointerFrame(p *wayland.Pointer) { | ||
| 440 | if p.Version() >= 5 { | 456 | if p.Version() >= 5 { | ||
| 441 | p.SendFrame() | 457 | p.SendFrame() | ||
| @@ -455,14 +471,12 @@ func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer { | |||||
| 455 | return s.clientPointers(l.resource.Client()) | 471 | return s.clientPointers(l.resource.Client()) | ||
| 456 | } | 472 | } | ||
| 457 | 473 | ||||
| 474 | + | // clientPointers returns c's wl_pointers without allocating. The list holds | |||
| 475 | + | // only live resources (OnDestroy removes a pointer when it dies) and is | |||
| 476 | + | // replaced, never mutated, so callers may iterate it while sending. See | |||
| 477 | + | // clientKeyboards for the libwayland invariant this relies on. | |||
| 458 | func (s *Server) clientPointers(c server.Client) []*wayland.Pointer { | 478 | func (s *Server) clientPointers(c server.Client) []*wayland.Pointer { | ||
| 459 | - | var result []*wayland.Pointer | 479 | + | return s.seat.pointers[c] |
| 460 | - | for _, p := range s.seat.pointers[c] { | |||
| 461 | - | if p.Resource.Alive() { | |||
| 462 | - | result = append(result, p) | |||
| 463 | - | } | |||
| 464 | - | } | |||
| 465 | - | return result | |||
| 466 | } | 480 | } | ||
| 467 | func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) { | 481 | func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) { | ||
| 468 | if s.security != nil && s.security.Snapshot().Protected && id != 0 && !s.lockInputTarget(id) { | 482 | if s.security != nil && s.security.Snapshot().Protected && id != 0 && !s.lockInputTarget(id) { | ||
M internal/adapters/wayland/server.go +2 -1
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | 11 | "github.com/bnema/go-wayland-bindings/server/relativepointer" | |
| 12 | 12 | ||
| 13 | 13 | "github.com/bnema/go-wayland-bindings/server/fractionalscale" | |
| 14 | + | "github.com/bnema/go-wayland-bindings/server/inputtimestamps" | |
| 14 | 15 | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 15 | 16 | "github.com/bnema/neferwl/internal/adapters/captureallow" | |
| 16 | 17 | "github.com/bnema/neferwl/internal/adapters/clock" | |
| @@ -331,7 +332,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 331 | 332 | relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, | |
| 332 | 333 | constraints: map[*surface]*constraint{}, | |
| 333 | 334 | positioners: map[*server.Resource]*positioner{}, | |
| 334 | - | seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}, | |
| 335 | + | seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), keyStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), pointerStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}, | |
| 335 | 336 | } | |
| 336 | 337 | s.leaseDevices = map[string]*leaseDevice{} | |
| 337 | 338 | s.pendingLeases = map[uint64]*leaseObject{} | |
| @@ -11,6 +11,7 @@ import ( | |||||
| 11 | "github.com/bnema/go-wayland-bindings/server/relativepointer" | 11 | "github.com/bnema/go-wayland-bindings/server/relativepointer" | ||
| 12 | 12 | ||||
| 13 | "github.com/bnema/go-wayland-bindings/server/fractionalscale" | 13 | "github.com/bnema/go-wayland-bindings/server/fractionalscale" | ||
| 14 | + | "github.com/bnema/go-wayland-bindings/server/inputtimestamps" | |||
| 14 | "github.com/bnema/go-wayland-bindings/server/wayland" | 15 | "github.com/bnema/go-wayland-bindings/server/wayland" | ||
| 15 | "github.com/bnema/neferwl/internal/adapters/captureallow" | 16 | "github.com/bnema/neferwl/internal/adapters/captureallow" | ||
| 16 | "github.com/bnema/neferwl/internal/adapters/clock" | 17 | "github.com/bnema/neferwl/internal/adapters/clock" | ||
| @@ -331,7 +332,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 331 | relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, | 332 | relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, | ||
| 332 | constraints: map[*surface]*constraint{}, | 333 | constraints: map[*surface]*constraint{}, | ||
| 333 | positioners: map[*server.Resource]*positioner{}, | 334 | positioners: map[*server.Resource]*positioner{}, | ||
| 334 | - | seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}, | 335 | + | seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), keyStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), pointerStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}, |
| 335 | } | 336 | } | ||
| 336 | s.leaseDevices = map[string]*leaseDevice{} | 337 | s.leaseDevices = map[string]*leaseDevice{} | ||
| 337 | s.pendingLeases = map[uint64]*leaseObject{} | 338 | s.pendingLeases = map[uint64]*leaseObject{} | ||
M internal/adapters/wayland/session_lock_input_epoch_test.go +1 -1
| @@ -158,7 +158,7 @@ func TestSessionLockInputEpochHeldPasswordClearedBeforeDesktopEnter(t *testing.T | |||
| 158 | 158 | if *locked != 1 { | |
| 159 | 159 | t.Fatal("lock not confirmed") | |
| 160 | 160 | } | |
| 161 | - | password := ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 1, State: ports.ModState{Depressed: 65, Latched: 2, Locked: 16, Group: 1}} | |
| 161 | + | password := ports.KeyEvent{Keycode: 30, Pressed: true, Time: 1 * time.Millisecond, State: ports.ModState{Depressed: 65, Latched: 2, Locked: 16, Group: 1}} | |
| 162 | 162 | s.display.Do(func() { | |
| 163 | 163 | s.apply(ports.SecurityCommand{State: state, Command: ports.FocusWindow{ID: id}}) | |
| 164 | 164 | s.apply(ports.SecurityCommand{State: state, Command: ports.ForwardKey{ID: id, Key: password}}) | |
| @@ -158,7 +158,7 @@ func TestSessionLockInputEpochHeldPasswordClearedBeforeDesktopEnter(t *testing.T | |||||
| 158 | if *locked != 1 { | 158 | if *locked != 1 { | ||
| 159 | t.Fatal("lock not confirmed") | 159 | t.Fatal("lock not confirmed") | ||
| 160 | } | 160 | } | ||
| 161 | - | password := ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 1, State: ports.ModState{Depressed: 65, Latched: 2, Locked: 16, Group: 1}} | 161 | + | password := ports.KeyEvent{Keycode: 30, Pressed: true, Time: 1 * time.Millisecond, State: ports.ModState{Depressed: 65, Latched: 2, Locked: 16, Group: 1}} |
| 162 | s.display.Do(func() { | 162 | s.display.Do(func() { | ||
| 163 | s.apply(ports.SecurityCommand{State: state, Command: ports.FocusWindow{ID: id}}) | 163 | s.apply(ports.SecurityCommand{State: state, Command: ports.FocusWindow{ID: id}}) | ||
| 164 | s.apply(ports.SecurityCommand{State: state, Command: ports.ForwardKey{ID: id, Key: password}}) | 164 | s.apply(ports.SecurityCommand{State: state, Command: ports.ForwardKey{ID: id, Key: password}}) | ||
M internal/adapters/wayland/session_lock_lifecycle_test.go +1 -1
| @@ -335,7 +335,7 @@ func TestSessionLockLifecycleIMEGrabCannotReceiveLockerKey(t *testing.T) { | |||
| 335 | 335 | s.display.Do(func() { | |
| 336 | 336 | l := s.surfaceOf(findTestSurface(s, surf.ID())).lock | |
| 337 | 337 | s.apply(ports.SecurityCommand{State: state, Command: ports.FocusWindow{ID: l.id}}) | |
| 338 | - | s.apply(ports.SecurityCommand{State: state, Command: ports.ForwardKey{ID: l.id, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 1}}}) | |
| 338 | + | s.apply(ports.SecurityCommand{State: state, Command: ports.ForwardKey{ID: l.id, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 1 * time.Millisecond}}}) | |
| 339 | 339 | }) | |
| 340 | 340 | settle(t, locker, ime.c) | |
| 341 | 341 | for _, event := range grab.take() { | |
| @@ -335,7 +335,7 @@ func TestSessionLockLifecycleIMEGrabCannotReceiveLockerKey(t *testing.T) { | |||||
| 335 | s.display.Do(func() { | 335 | s.display.Do(func() { | ||
| 336 | l := s.surfaceOf(findTestSurface(s, surf.ID())).lock | 336 | l := s.surfaceOf(findTestSurface(s, surf.ID())).lock | ||
| 337 | s.apply(ports.SecurityCommand{State: state, Command: ports.FocusWindow{ID: l.id}}) | 337 | s.apply(ports.SecurityCommand{State: state, Command: ports.FocusWindow{ID: l.id}}) | ||
| 338 | - | s.apply(ports.SecurityCommand{State: state, Command: ports.ForwardKey{ID: l.id, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 1}}}) | 338 | + | s.apply(ports.SecurityCommand{State: state, Command: ports.ForwardKey{ID: l.id, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 1 * time.Millisecond}}}) |
| 339 | }) | 339 | }) | ||
| 340 | settle(t, locker, ime.c) | 340 | settle(t, locker, ime.c) | ||
| 341 | for _, event := range grab.take() { | 341 | for _, event := range grab.take() { | ||
M internal/adapters/wayland/textinput.go +1 -1
| @@ -516,7 +516,7 @@ func (s *Server) grabKey(c ports.ForwardKey) bool { | |||
| 516 | 516 | s.seat.grabKeys[code] = true | |
| 517 | 517 | } | |
| 518 | 518 | s.serial++ | |
| 519 | - | g.res.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state) | |
| 519 | + | g.res.SendKey(s.serial, wireMsec(c.Key.Time), c.Key.Keycode, state) | |
| 520 | 520 | if c.Key.State != s.seat.modState { | |
| 521 | 521 | s.seat.modState = c.Key.State | |
| 522 | 522 | m := s.seat.modState | |
| @@ -516,7 +516,7 @@ func (s *Server) grabKey(c ports.ForwardKey) bool { | |||||
| 516 | s.seat.grabKeys[code] = true | 516 | s.seat.grabKeys[code] = true | ||
| 517 | } | 517 | } | ||
| 518 | s.serial++ | 518 | s.serial++ | ||
| 519 | - | g.res.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state) | 519 | + | g.res.SendKey(s.serial, wireMsec(c.Key.Time), c.Key.Keycode, state) |
| 520 | if c.Key.State != s.seat.modState { | 520 | if c.Key.State != s.seat.modState { | ||
| 521 | s.seat.modState = c.Key.State | 521 | s.seat.modState = c.Key.State | ||
| 522 | m := s.seat.modState | 522 | m := s.seat.modState | ||
M internal/adapters/wayland/textinput_test.go +1 -1
| @@ -309,7 +309,7 @@ func TestInputMethodKeyboardGrab(t *testing.T) { | |||
| 309 | 309 | ime := newIMEApp(t, s, dir) | |
| 310 | 310 | app.keys.opcodes = nil | |
| 311 | 311 | key := func(code uint32, pressed bool) { | |
| 312 | - | commands <- ports.ForwardKey{ID: app.window, Key: ports.KeyEvent{Keycode: code, Pressed: pressed, TimeMsec: 7}} | |
| 312 | + | commands <- ports.ForwardKey{ID: app.window, Key: ports.KeyEvent{Keycode: code, Pressed: pressed, Time: 7 * time.Millisecond}} | |
| 313 | 313 | } | |
| 314 | 314 | k := uint16(wayland.KeyboardEventKey) | |
| 315 | 315 | key(31, true) // held by the client before the grab | |
| @@ -309,7 +309,7 @@ func TestInputMethodKeyboardGrab(t *testing.T) { | |||||
| 309 | ime := newIMEApp(t, s, dir) | 309 | ime := newIMEApp(t, s, dir) | ||
| 310 | app.keys.opcodes = nil | 310 | app.keys.opcodes = nil | ||
| 311 | key := func(code uint32, pressed bool) { | 311 | key := func(code uint32, pressed bool) { | ||
| 312 | - | commands <- ports.ForwardKey{ID: app.window, Key: ports.KeyEvent{Keycode: code, Pressed: pressed, TimeMsec: 7}} | 312 | + | commands <- ports.ForwardKey{ID: app.window, Key: ports.KeyEvent{Keycode: code, Pressed: pressed, Time: 7 * time.Millisecond}} |
| 313 | } | 313 | } | ||
| 314 | k := uint16(wayland.KeyboardEventKey) | 314 | k := uint16(wayland.KeyboardEventKey) | ||
| 315 | key(31, true) // held by the client before the grab | 315 | key(31, true) // held by the client before the grab | ||
M internal/adapters/wayland/virtualkeyboard.go +9 -5
| @@ -136,7 +136,7 @@ func (k *virtualKeyboard) keyboards(r *virtualkeyboard.ZwpVirtualKeyboardV1) []* | |||
| 136 | 136 | return keyboards | |
| 137 | 137 | } | |
| 138 | 138 | ||
| 139 | - | func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key, state uint32) { | |
| 139 | + | func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, msec, key, state uint32) { | |
| 140 | 140 | if !k.admitted() || state != 1 && k.dropReleases && !k.pressed[key] { | |
| 141 | 141 | return | |
| 142 | 142 | } | |
| @@ -149,8 +149,11 @@ func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key | |||
| 149 | 149 | } else { | |
| 150 | 150 | delete(k.pressed, key) | |
| 151 | 151 | } | |
| 152 | + | // The client's millisecond time is all the timestamp there is. | |
| 153 | + | stamp := time.Duration(msec) * time.Millisecond | |
| 152 | 154 | for _, kb := range keyboards { | |
| 153 | - | kb.SendKey(k.server.serial, time, key, state) | |
| 155 | + | k.server.stampKeyboard(kb, stamp) | |
| 156 | + | kb.SendKey(k.server.serial, msec, key, state) | |
| 154 | 157 | } | |
| 155 | 158 | } | |
| 156 | 159 | ||
| @@ -198,12 +201,13 @@ func (k *virtualKeyboard) release() { | |||
| 198 | 201 | _, keyboards := s.focusTarget(s.seat.focused) | |
| 199 | 202 | var ts unix.Timespec | |
| 200 | 203 | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | |
| 201 | - | // Input events carry CLOCK_MONOTONIC milliseconds (libinput's clock). | |
| 202 | - | now := uint32(ts.Nano() / int64(time.Millisecond)) | |
| 204 | + | // Input events carry CLOCK_MONOTONIC (libinput's clock). | |
| 205 | + | now := time.Duration(ts.Nano()) | |
| 203 | 206 | for key := range k.pressed { | |
| 204 | 207 | s.serial++ | |
| 205 | 208 | for _, kb := range keyboards { | |
| 206 | - | kb.SendKey(s.serial, now, key, 0) | |
| 209 | + | s.stampKeyboard(kb, now) | |
| 210 | + | kb.SendKey(s.serial, wireMsec(now), key, 0) | |
| 207 | 211 | } | |
| 208 | 212 | } | |
| 209 | 213 | } | |
| @@ -136,7 +136,7 @@ func (k *virtualKeyboard) keyboards(r *virtualkeyboard.ZwpVirtualKeyboardV1) []* | |||||
| 136 | return keyboards | 136 | return keyboards | ||
| 137 | } | 137 | } | ||
| 138 | 138 | ||||
| 139 | - | func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key, state uint32) { | 139 | + | func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, msec, key, state uint32) { |
| 140 | if !k.admitted() || state != 1 && k.dropReleases && !k.pressed[key] { | 140 | if !k.admitted() || state != 1 && k.dropReleases && !k.pressed[key] { | ||
| 141 | return | 141 | return | ||
| 142 | } | 142 | } | ||
| @@ -149,8 +149,11 @@ func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key | |||||
| 149 | } else { | 149 | } else { | ||
| 150 | delete(k.pressed, key) | 150 | delete(k.pressed, key) | ||
| 151 | } | 151 | } | ||
| 152 | + | // The client's millisecond time is all the timestamp there is. | |||
| 153 | + | stamp := time.Duration(msec) * time.Millisecond | |||
| 152 | for _, kb := range keyboards { | 154 | for _, kb := range keyboards { | ||
| 153 | - | kb.SendKey(k.server.serial, time, key, state) | 155 | + | k.server.stampKeyboard(kb, stamp) |
| 156 | + | kb.SendKey(k.server.serial, msec, key, state) | |||
| 154 | } | 157 | } | ||
| 155 | } | 158 | } | ||
| 156 | 159 | ||||
| @@ -198,12 +201,13 @@ func (k *virtualKeyboard) release() { | |||||
| 198 | _, keyboards := s.focusTarget(s.seat.focused) | 201 | _, keyboards := s.focusTarget(s.seat.focused) | ||
| 199 | var ts unix.Timespec | 202 | var ts unix.Timespec | ||
| 200 | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | 203 | _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts) | ||
| 201 | - | // Input events carry CLOCK_MONOTONIC milliseconds (libinput's clock). | 204 | + | // Input events carry CLOCK_MONOTONIC (libinput's clock). |
| 202 | - | now := uint32(ts.Nano() / int64(time.Millisecond)) | 205 | + | now := time.Duration(ts.Nano()) |
| 203 | for key := range k.pressed { | 206 | for key := range k.pressed { | ||
| 204 | s.serial++ | 207 | s.serial++ | ||
| 205 | for _, kb := range keyboards { | 208 | for _, kb := range keyboards { | ||
| 206 | - | kb.SendKey(s.serial, now, key, 0) | 209 | + | s.stampKeyboard(kb, now) |
| 210 | + | kb.SendKey(s.serial, wireMsec(now), key, 0) | |||
| 207 | } | 211 | } | ||
| 208 | } | 212 | } | ||
| 209 | } | 213 | } | ||
M internal/adapters/wayland/virtualkeyboard_test.go +1 -1
| @@ -78,7 +78,7 @@ func TestVirtualKeyboardTypesIntoFocus(t *testing.T) { | |||
| 78 | 78 | } | |
| 79 | 79 | ||
| 80 | 80 | proxy.opcodes, proxy.keymaps = nil, nil | |
| 81 | - | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 3}} | |
| 81 | + | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 3 * time.Millisecond}} | |
| 82 | 82 | deadline := time.Now().Add(2 * time.Second) | |
| 83 | 83 | for !slices.Contains(proxy.opcodes, uint16(wayland.KeyboardEventKey)) && time.Now().Before(deadline) { | |
| 84 | 84 | if err := target.Roundtrip(); err != nil { | |
| @@ -78,7 +78,7 @@ func TestVirtualKeyboardTypesIntoFocus(t *testing.T) { | |||||
| 78 | } | 78 | } | ||
| 79 | 79 | ||||
| 80 | proxy.opcodes, proxy.keymaps = nil, nil | 80 | proxy.opcodes, proxy.keymaps = nil, nil | ||
| 81 | - | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 3}} | 81 | + | commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 3 * time.Millisecond}} |
| 82 | deadline := time.Now().Add(2 * time.Second) | 82 | deadline := time.Now().Add(2 * time.Second) | ||
| 83 | for !slices.Contains(proxy.opcodes, uint16(wayland.KeyboardEventKey)) && time.Now().Before(deadline) { | 83 | for !slices.Contains(proxy.opcodes, uint16(wayland.KeyboardEventKey)) && time.Now().Before(deadline) { | ||
| 84 | if err := target.Roundtrip(); err != nil { | 84 | if err := target.Roundtrip(); err != nil { | ||
M internal/adapters/wayland/wire_client_test.go +3 -0
| @@ -27,6 +27,7 @@ import ( | |||
| 27 | 27 | wire_fractionalscale "github.com/bnema/go-wayland-bindings/server/fractionalscale" | |
| 28 | 28 | wire_idleinhibit "github.com/bnema/go-wayland-bindings/server/idleinhibit" | |
| 29 | 29 | wire_inputmethod "github.com/bnema/go-wayland-bindings/server/inputmethod" | |
| 30 | + | wire_inputtimestamps "github.com/bnema/go-wayland-bindings/server/inputtimestamps" | |
| 30 | 31 | wire_keyboardshortcutsinhibit "github.com/bnema/go-wayland-bindings/server/keyboardshortcutsinhibit" | |
| 31 | 32 | wire_linuxdmabuf "github.com/bnema/go-wayland-bindings/server/linuxdmabuf" | |
| 32 | 33 | wire_linuxdrmsyncobj "github.com/bnema/go-wayland-bindings/server/linuxdrmsyncobj" | |
| @@ -225,6 +226,8 @@ var wireSchemas = map[string]wireSchema{ | |||
| 225 | 226 | "zwp_input_method_manager_v2": {wire_inputmethod.ZwpInputMethodManagerV2Interface, &client_inputmethod.InputMethodManager{}, map[uint16]bool{1: true}, nil}, | |
| 226 | 227 | "zwp_input_method_v2": {wire_inputmethod.ZwpInputMethodV2Interface, &client_inputmethod.InputMethod{}, map[uint16]bool{6: true}, nil}, | |
| 227 | 228 | "zwp_input_popup_surface_v2": {wire_inputmethod.ZwpInputPopupSurfaceV2Interface, &client_inputmethod.InputPopupSurface{}, map[uint16]bool{0: true}, nil}, | |
| 229 | + | "zwp_input_timestamps_manager_v1": {wire_inputtimestamps.ZwpInputTimestampsManagerV1Interface, nil, map[uint16]bool{0: true}, nil}, | |
| 230 | + | "zwp_input_timestamps_v1": {wire_inputtimestamps.ZwpInputTimestampsV1Interface, nil, map[uint16]bool{0: true}, nil}, | |
| 228 | 231 | "zwp_keyboard_shortcuts_inhibit_manager_v1": {wire_keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1Interface, &client_shortcutsinhibit.KeyboardShortcutsInhibitManager{}, map[uint16]bool{0: true}, nil}, | |
| 229 | 232 | "zwp_keyboard_shortcuts_inhibitor_v1": {wire_keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1Interface, &client_shortcutsinhibit.KeyboardShortcutsInhibitor{}, map[uint16]bool{0: true}, nil}, | |
| 230 | 233 | "zwp_linux_buffer_params_v1": {wire_linuxdmabuf.ZwpLinuxBufferParamsV1Interface, &client_linuxdmabuf.LinuxBufferParams{}, map[uint16]bool{0: true}, nil}, | |
| @@ -27,6 +27,7 @@ import ( | |||||
| 27 | wire_fractionalscale "github.com/bnema/go-wayland-bindings/server/fractionalscale" | 27 | wire_fractionalscale "github.com/bnema/go-wayland-bindings/server/fractionalscale" | ||
| 28 | wire_idleinhibit "github.com/bnema/go-wayland-bindings/server/idleinhibit" | 28 | wire_idleinhibit "github.com/bnema/go-wayland-bindings/server/idleinhibit" | ||
| 29 | wire_inputmethod "github.com/bnema/go-wayland-bindings/server/inputmethod" | 29 | wire_inputmethod "github.com/bnema/go-wayland-bindings/server/inputmethod" | ||
| 30 | + | wire_inputtimestamps "github.com/bnema/go-wayland-bindings/server/inputtimestamps" | |||
| 30 | wire_keyboardshortcutsinhibit "github.com/bnema/go-wayland-bindings/server/keyboardshortcutsinhibit" | 31 | wire_keyboardshortcutsinhibit "github.com/bnema/go-wayland-bindings/server/keyboardshortcutsinhibit" | ||
| 31 | wire_linuxdmabuf "github.com/bnema/go-wayland-bindings/server/linuxdmabuf" | 32 | wire_linuxdmabuf "github.com/bnema/go-wayland-bindings/server/linuxdmabuf" | ||
| 32 | wire_linuxdrmsyncobj "github.com/bnema/go-wayland-bindings/server/linuxdrmsyncobj" | 33 | wire_linuxdrmsyncobj "github.com/bnema/go-wayland-bindings/server/linuxdrmsyncobj" | ||
| @@ -225,6 +226,8 @@ var wireSchemas = map[string]wireSchema{ | |||||
| 225 | "zwp_input_method_manager_v2": {wire_inputmethod.ZwpInputMethodManagerV2Interface, &client_inputmethod.InputMethodManager{}, map[uint16]bool{1: true}, nil}, | 226 | "zwp_input_method_manager_v2": {wire_inputmethod.ZwpInputMethodManagerV2Interface, &client_inputmethod.InputMethodManager{}, map[uint16]bool{1: true}, nil}, | ||
| 226 | "zwp_input_method_v2": {wire_inputmethod.ZwpInputMethodV2Interface, &client_inputmethod.InputMethod{}, map[uint16]bool{6: true}, nil}, | 227 | "zwp_input_method_v2": {wire_inputmethod.ZwpInputMethodV2Interface, &client_inputmethod.InputMethod{}, map[uint16]bool{6: true}, nil}, | ||
| 227 | "zwp_input_popup_surface_v2": {wire_inputmethod.ZwpInputPopupSurfaceV2Interface, &client_inputmethod.InputPopupSurface{}, map[uint16]bool{0: true}, nil}, | 228 | "zwp_input_popup_surface_v2": {wire_inputmethod.ZwpInputPopupSurfaceV2Interface, &client_inputmethod.InputPopupSurface{}, map[uint16]bool{0: true}, nil}, | ||
| 229 | + | "zwp_input_timestamps_manager_v1": {wire_inputtimestamps.ZwpInputTimestampsManagerV1Interface, nil, map[uint16]bool{0: true}, nil}, | |||
| 230 | + | "zwp_input_timestamps_v1": {wire_inputtimestamps.ZwpInputTimestampsV1Interface, nil, map[uint16]bool{0: true}, nil}, | |||
| 228 | "zwp_keyboard_shortcuts_inhibit_manager_v1": {wire_keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1Interface, &client_shortcutsinhibit.KeyboardShortcutsInhibitManager{}, map[uint16]bool{0: true}, nil}, | 231 | "zwp_keyboard_shortcuts_inhibit_manager_v1": {wire_keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1Interface, &client_shortcutsinhibit.KeyboardShortcutsInhibitManager{}, map[uint16]bool{0: true}, nil}, | ||
| 229 | "zwp_keyboard_shortcuts_inhibitor_v1": {wire_keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1Interface, &client_shortcutsinhibit.KeyboardShortcutsInhibitor{}, map[uint16]bool{0: true}, nil}, | 232 | "zwp_keyboard_shortcuts_inhibitor_v1": {wire_keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1Interface, &client_shortcutsinhibit.KeyboardShortcutsInhibitor{}, map[uint16]bool{0: true}, nil}, | ||
| 230 | "zwp_linux_buffer_params_v1": {wire_linuxdmabuf.ZwpLinuxBufferParamsV1Interface, &client_linuxdmabuf.LinuxBufferParams{}, map[uint16]bool{0: true}, nil}, | 233 | "zwp_linux_buffer_params_v1": {wire_linuxdmabuf.ZwpLinuxBufferParamsV1Interface, &client_linuxdmabuf.LinuxBufferParams{}, map[uint16]bool{0: true}, nil}, | ||
M internal/adapters/xkb/xkb.go +5 -4
| @@ -4,6 +4,7 @@ package xkb | |||
| 4 | 4 | import ( | |
| 5 | 5 | "fmt" | |
| 6 | 6 | "runtime" | |
| 7 | + | "time" | |
| 7 | 8 | "unsafe" | |
| 8 | 9 | ||
| 9 | 10 | "github.com/bnema/neferwl/internal/ports" | |
| @@ -130,7 +131,7 @@ func (k *Keymap) String() string { | |||
| 130 | 131 | return string(unsafe.Slice((*byte)(ptr), n)) | |
| 131 | 132 | } | |
| 132 | 133 | ||
| 133 | - | func (k *Keymap) Key(evdevCode uint32, pressed bool, timeMsec uint32) ports.KeyEvent { | |
| 134 | + | func (k *Keymap) Key(evdevCode uint32, pressed bool, t time.Duration) ports.KeyEvent { | |
| 134 | 135 | code := evdevCode + 8 | |
| 135 | 136 | sym := k.getSym(k.state, code) | |
| 136 | 137 | var buf [64]byte | |
| @@ -144,7 +145,7 @@ func (k *Keymap) Key(evdevCode uint32, pressed bool, timeMsec uint32) ports.KeyE | |||
| 144 | 145 | direction = 1 | |
| 145 | 146 | } | |
| 146 | 147 | k.updateKey(k.state, code, direction) | |
| 147 | - | event := ports.KeyEvent{Keysym: name, Pressed: pressed, TimeMsec: timeMsec, Keycode: evdevCode} | |
| 148 | + | event := ports.KeyEvent{Keysym: name, Pressed: pressed, Time: t, Keycode: evdevCode} | |
| 148 | 149 | if pressed { | |
| 149 | 150 | layout := k.serializeLayout(k.state, layoutEffective) | |
| 150 | 151 | event.Base = k.levelName(code, layout, 0) | |
| @@ -170,7 +171,7 @@ func (k *Keymap) Key(evdevCode uint32, pressed bool, timeMsec uint32) ports.KeyE | |||
| 170 | 171 | // new security epoch. Keys physically held across a transition are suppressed | |
| 171 | 172 | // (including repeats and their eventual release), never replayed into XKB. | |
| 172 | 173 | // On reset failure no event is translated against the old native state. | |
| 173 | - | func (k *Keymap) KeySecure(code uint32, pressed bool, timeMsec uint32, epoch ports.SecurityState) (ports.KeyEvent, bool, error) { | |
| 174 | + | func (k *Keymap) KeySecure(code uint32, pressed bool, t time.Duration, epoch ports.SecurityState) (ports.KeyEvent, bool, error) { | |
| 174 | 175 | if !k.epochSet || k.epoch != epoch { | |
| 175 | 176 | next := k.stateNew(k.mapPtr) | |
| 176 | 177 | if next == 0 { | |
| @@ -208,7 +209,7 @@ func (k *Keymap) KeySecure(code uint32, pressed bool, timeMsec uint32, epoch por | |||
| 208 | 209 | if blocked { | |
| 209 | 210 | return ports.KeyEvent{}, false, nil | |
| 210 | 211 | } | |
| 211 | - | return k.Key(code, pressed, timeMsec), true, nil | |
| 212 | + | return k.Key(code, pressed, t), true, nil | |
| 212 | 213 | } | |
| 213 | 214 | ||
| 214 | 215 | // QuarantineFrom transfers physical held-key tracking when a producer replaces | |
| @@ -4,6 +4,7 @@ package xkb | |||||
| 4 | import ( | 4 | import ( | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "runtime" | 6 | "runtime" | ||
| 7 | + | "time" | |||
| 7 | "unsafe" | 8 | "unsafe" | ||
| 8 | 9 | ||||
| 9 | "github.com/bnema/neferwl/internal/ports" | 10 | "github.com/bnema/neferwl/internal/ports" | ||
| @@ -130,7 +131,7 @@ func (k *Keymap) String() string { | |||||
| 130 | return string(unsafe.Slice((*byte)(ptr), n)) | 131 | return string(unsafe.Slice((*byte)(ptr), n)) | ||
| 131 | } | 132 | } | ||
| 132 | 133 | ||||
| 133 | - | func (k *Keymap) Key(evdevCode uint32, pressed bool, timeMsec uint32) ports.KeyEvent { | 134 | + | func (k *Keymap) Key(evdevCode uint32, pressed bool, t time.Duration) ports.KeyEvent { |
| 134 | code := evdevCode + 8 | 135 | code := evdevCode + 8 | ||
| 135 | sym := k.getSym(k.state, code) | 136 | sym := k.getSym(k.state, code) | ||
| 136 | var buf [64]byte | 137 | var buf [64]byte | ||
| @@ -144,7 +145,7 @@ func (k *Keymap) Key(evdevCode uint32, pressed bool, timeMsec uint32) ports.KeyE | |||||
| 144 | direction = 1 | 145 | direction = 1 | ||
| 145 | } | 146 | } | ||
| 146 | k.updateKey(k.state, code, direction) | 147 | k.updateKey(k.state, code, direction) | ||
| 147 | - | event := ports.KeyEvent{Keysym: name, Pressed: pressed, TimeMsec: timeMsec, Keycode: evdevCode} | 148 | + | event := ports.KeyEvent{Keysym: name, Pressed: pressed, Time: t, Keycode: evdevCode} |
| 148 | if pressed { | 149 | if pressed { | ||
| 149 | layout := k.serializeLayout(k.state, layoutEffective) | 150 | layout := k.serializeLayout(k.state, layoutEffective) | ||
| 150 | event.Base = k.levelName(code, layout, 0) | 151 | event.Base = k.levelName(code, layout, 0) | ||
| @@ -170,7 +171,7 @@ func (k *Keymap) Key(evdevCode uint32, pressed bool, timeMsec uint32) ports.KeyE | |||||
| 170 | // new security epoch. Keys physically held across a transition are suppressed | 171 | // new security epoch. Keys physically held across a transition are suppressed | ||
| 171 | // (including repeats and their eventual release), never replayed into XKB. | 172 | // (including repeats and their eventual release), never replayed into XKB. | ||
| 172 | // On reset failure no event is translated against the old native state. | 173 | // On reset failure no event is translated against the old native state. | ||
| 173 | - | func (k *Keymap) KeySecure(code uint32, pressed bool, timeMsec uint32, epoch ports.SecurityState) (ports.KeyEvent, bool, error) { | 174 | + | func (k *Keymap) KeySecure(code uint32, pressed bool, t time.Duration, epoch ports.SecurityState) (ports.KeyEvent, bool, error) { |
| 174 | if !k.epochSet || k.epoch != epoch { | 175 | if !k.epochSet || k.epoch != epoch { | ||
| 175 | next := k.stateNew(k.mapPtr) | 176 | next := k.stateNew(k.mapPtr) | ||
| 176 | if next == 0 { | 177 | if next == 0 { | ||
| @@ -208,7 +209,7 @@ func (k *Keymap) KeySecure(code uint32, pressed bool, timeMsec uint32, epoch por | |||||
| 208 | if blocked { | 209 | if blocked { | ||
| 209 | return ports.KeyEvent{}, false, nil | 210 | return ports.KeyEvent{}, false, nil | ||
| 210 | } | 211 | } | ||
| 211 | - | return k.Key(code, pressed, timeMsec), true, nil | 212 | + | return k.Key(code, pressed, t), true, nil |
| 212 | } | 213 | } | ||
| 213 | 214 | ||||
| 214 | // QuarantineFrom transfers physical held-key tracking when a producer replaces | 215 | // QuarantineFrom transfers physical held-key tracking when a producer replaces | ||
M internal/adapters/xkb/xkb_test.go +2 -1
| @@ -3,6 +3,7 @@ package xkb | |||
| 3 | 3 | import ( | |
| 4 | 4 | "strings" | |
| 5 | 5 | "testing" | |
| 6 | + | "time" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/neferwl/internal/ports" | |
| 8 | 9 | ) | |
| @@ -25,7 +26,7 @@ func TestKeymap(t *testing.T) { | |||
| 25 | 26 | if !strings.Contains(k.String(), "xkb_keymap") { | |
| 26 | 27 | t.Fatal("missing keymap text") | |
| 27 | 28 | } | |
| 28 | - | if ev := k.Key(30, true, 123); ev.Keysym != "a" || ev.Keycode != 30 || !ev.Pressed || ev.TimeMsec != 123 { | |
| 29 | + | if ev := k.Key(30, true, 123*time.Millisecond); ev.Keysym != "a" || ev.Keycode != 30 || !ev.Pressed || ev.Time != 123*time.Millisecond { | |
| 29 | 30 | t.Fatalf("A: %+v", ev) | |
| 30 | 31 | } | |
| 31 | 32 | k.Key(30, false, 0) | |
| @@ -3,6 +3,7 @@ package xkb | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "strings" | 4 | "strings" | ||
| 5 | "testing" | 5 | "testing" | ||
| 6 | + | "time" | |||
| 6 | 7 | ||||
| 7 | "github.com/bnema/neferwl/internal/ports" | 8 | "github.com/bnema/neferwl/internal/ports" | ||
| 8 | ) | 9 | ) | ||
| @@ -25,7 +26,7 @@ func TestKeymap(t *testing.T) { | |||||
| 25 | if !strings.Contains(k.String(), "xkb_keymap") { | 26 | if !strings.Contains(k.String(), "xkb_keymap") { | ||
| 26 | t.Fatal("missing keymap text") | 27 | t.Fatal("missing keymap text") | ||
| 27 | } | 28 | } | ||
| 28 | - | if ev := k.Key(30, true, 123); ev.Keysym != "a" || ev.Keycode != 30 || !ev.Pressed || ev.TimeMsec != 123 { | 29 | + | if ev := k.Key(30, true, 123*time.Millisecond); ev.Keysym != "a" || ev.Keycode != 30 || !ev.Pressed || ev.Time != 123*time.Millisecond { |
| 29 | t.Fatalf("A: %+v", ev) | 30 | t.Fatalf("A: %+v", ev) | ||
| 30 | } | 31 | } | ||
| 31 | k.Key(30, false, 0) | 32 | k.Key(30, false, 0) | ||
M internal/core/core.go +3 -3
| @@ -130,9 +130,9 @@ type Core struct { | |||
| 130 | 130 | swipe *swipeGesture | |
| 131 | 131 | frameC <-chan time.Time | |
| 132 | 132 | frameStop func() bool | |
| 133 | - | // motionMsec is the time of the last pointer motion; pointerAt is the | |
| 133 | + | // motionTime is the time of the last pointer motion; pointerAt is the | |
| 134 | 134 | // last position sent in the pointer's window. | |
| 135 | - | motionMsec uint32 | |
| 135 | + | motionTime time.Duration | |
| 136 | 136 | pointerAt [2]float64 | |
| 137 | 137 | // capt is the capture state (capture.go, capindicator.go). | |
| 138 | 138 | capt captureState | |
| @@ -703,7 +703,7 @@ func (c *Core) rehit(ctx context.Context) error { | |||
| 703 | 703 | // The same window moved under the cursor: it moves in the window. No | |
| 704 | 704 | // relative motion: the device did not move. The input clock is the | |
| 705 | 705 | // device's; the last motion's time is the closest core knows. | |
| 706 | - | return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, TimeMsec: c.motionMsec}) | |
| 706 | + | return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, Time: c.motionTime}) | |
| 707 | 707 | } | |
| 708 | 708 | ||
| 709 | 709 | // updateInhibit makes the shortcuts inhibitor of the keyboard focus the | |
| @@ -130,9 +130,9 @@ type Core struct { | |||||
| 130 | swipe *swipeGesture | 130 | swipe *swipeGesture | ||
| 131 | frameC <-chan time.Time | 131 | frameC <-chan time.Time | ||
| 132 | frameStop func() bool | 132 | frameStop func() bool | ||
| 133 | - | // motionMsec is the time of the last pointer motion; pointerAt is the | 133 | + | // motionTime is the time of the last pointer motion; pointerAt is the |
| 134 | // last position sent in the pointer's window. | 134 | // last position sent in the pointer's window. | ||
| 135 | - | motionMsec uint32 | 135 | + | motionTime time.Duration |
| 136 | pointerAt [2]float64 | 136 | pointerAt [2]float64 | ||
| 137 | // capt is the capture state (capture.go, capindicator.go). | 137 | // capt is the capture state (capture.go, capindicator.go). | ||
| 138 | capt captureState | 138 | capt captureState | ||
| @@ -703,7 +703,7 @@ func (c *Core) rehit(ctx context.Context) error { | |||||
| 703 | // The same window moved under the cursor: it moves in the window. No | 703 | // The same window moved under the cursor: it moves in the window. No | ||
| 704 | // relative motion: the device did not move. The input clock is the | 704 | // relative motion: the device did not move. The input clock is the | ||
| 705 | // device's; the last motion's time is the closest core knows. | 705 | // device's; the last motion's time is the closest core knows. | ||
| 706 | - | return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, TimeMsec: c.motionMsec}) | 706 | + | return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, Time: c.motionTime}) |
| 707 | } | 707 | } | ||
| 708 | 708 | ||||
| 709 | // updateInhibit makes the shortcuts inhibitor of the keyboard focus the | 709 | // updateInhibit makes the shortcuts inhibitor of the keyboard focus the | ||
M internal/core/core_test.go +3 -3
| @@ -245,11 +245,11 @@ func TestPointerFocusAndGrab(t *testing.T) { | |||
| 245 | 245 | for len(commands) > 0 { | |
| 246 | 246 | <-commands | |
| 247 | 247 | } | |
| 248 | - | input <- ports.PointerMotion{X: float64(first.X + 2), Y: float64(first.Y + 3), TimeMsec: 1} | |
| 248 | + | input <- ports.PointerMotion{X: float64(first.X + 2), Y: float64(first.Y + 3), Time: 1 * time.Millisecond} | |
| 249 | 249 | if v := command(t, commands); v != (ports.PointerFocus{ID: 1, X: 2, Y: 3}) { | |
| 250 | 250 | t.Fatal(v) | |
| 251 | 251 | } | |
| 252 | - | if v := command(t, commands); v != (ports.PointerMotionTo{ID: 1, X: 2, Y: 3, TimeMsec: 1}) { | |
| 252 | + | if v := command(t, commands); v != (ports.PointerMotionTo{ID: 1, X: 2, Y: 3, Time: 1 * time.Millisecond}) { | |
| 253 | 253 | t.Fatal(v) | |
| 254 | 254 | } | |
| 255 | 255 | input <- ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}} | |
| @@ -324,7 +324,7 @@ func TestBorderInset(t *testing.T) { | |||
| 324 | 324 | if !found { | |
| 325 | 325 | t.Fatalf("no %v for outer %v", want, r) | |
| 326 | 326 | } | |
| 327 | - | input <- ports.PointerMotion{X: float64(r.X + 2), Y: float64(r.Y + 5), TimeMsec: 1} | |
| 327 | + | input <- ports.PointerMotion{X: float64(r.X + 2), Y: float64(r.Y + 5), Time: 1 * time.Millisecond} | |
| 328 | 328 | if v := command(t, commands); v != (ports.PointerFocus{ID: 2, X: 2, Y: 5}) { | |
| 329 | 329 | t.Fatal(v) | |
| 330 | 330 | } | |
| @@ -245,11 +245,11 @@ func TestPointerFocusAndGrab(t *testing.T) { | |||||
| 245 | for len(commands) > 0 { | 245 | for len(commands) > 0 { | ||
| 246 | <-commands | 246 | <-commands | ||
| 247 | } | 247 | } | ||
| 248 | - | input <- ports.PointerMotion{X: float64(first.X + 2), Y: float64(first.Y + 3), TimeMsec: 1} | 248 | + | input <- ports.PointerMotion{X: float64(first.X + 2), Y: float64(first.Y + 3), Time: 1 * time.Millisecond} |
| 249 | if v := command(t, commands); v != (ports.PointerFocus{ID: 1, X: 2, Y: 3}) { | 249 | if v := command(t, commands); v != (ports.PointerFocus{ID: 1, X: 2, Y: 3}) { | ||
| 250 | t.Fatal(v) | 250 | t.Fatal(v) | ||
| 251 | } | 251 | } | ||
| 252 | - | if v := command(t, commands); v != (ports.PointerMotionTo{ID: 1, X: 2, Y: 3, TimeMsec: 1}) { | 252 | + | if v := command(t, commands); v != (ports.PointerMotionTo{ID: 1, X: 2, Y: 3, Time: 1 * time.Millisecond}) { |
| 253 | t.Fatal(v) | 253 | t.Fatal(v) | ||
| 254 | } | 254 | } | ||
| 255 | input <- ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}} | 255 | input <- ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}} | ||
| @@ -324,7 +324,7 @@ func TestBorderInset(t *testing.T) { | |||||
| 324 | if !found { | 324 | if !found { | ||
| 325 | t.Fatalf("no %v for outer %v", want, r) | 325 | t.Fatalf("no %v for outer %v", want, r) | ||
| 326 | } | 326 | } | ||
| 327 | - | input <- ports.PointerMotion{X: float64(r.X + 2), Y: float64(r.Y + 5), TimeMsec: 1} | 327 | + | input <- ports.PointerMotion{X: float64(r.X + 2), Y: float64(r.Y + 5), Time: 1 * time.Millisecond} |
| 328 | if v := command(t, commands); v != (ports.PointerFocus{ID: 2, X: 2, Y: 5}) { | 328 | if v := command(t, commands); v != (ports.PointerFocus{ID: 2, X: 2, Y: 5}) { | ||
| 329 | t.Fatal(v) | 329 | t.Fatal(v) | ||
| 330 | } | 330 | } | ||
M internal/core/drag.go +10 -9
| @@ -3,6 +3,7 @@ package core | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "slices" | |
| 6 | + | "time" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/neferwl/internal/ports" | |
| 8 | 9 | ) | |
| @@ -30,9 +31,9 @@ const ( | |||
| 30 | 31 | gapPad = 12 | |
| 31 | 32 | // edgeScroll is the distance from the usable edge, logical px, that | |
| 32 | 33 | // scrolls a scroll-overflow workspace during a tile drag, at most | |
| 33 | - | // once per edgeScrollMsec. | |
| 34 | - | edgeScroll = 32 | |
| 35 | - | edgeScrollMsec = 250 | |
| 34 | + | // once per edgeScrollInterval. | |
| 35 | + | edgeScroll = 32 | |
| 36 | + | edgeScrollInterval = 250 * time.Millisecond | |
| 36 | 37 | ) | |
| 37 | 38 | ||
| 38 | 39 | type dropKind int | |
| @@ -69,7 +70,7 @@ type dragState struct { | |||
| 69 | 70 | float bool | |
| 70 | 71 | target dropTarget | |
| 71 | 72 | // scrolled is the time of the last edge scroll. | |
| 72 | - | scrolled uint32 | |
| 73 | + | scrolled time.Duration | |
| 73 | 74 | } | |
| 74 | 75 | ||
| 75 | 76 | // draggable returns the screen and workspace of a window a drag may | |
| @@ -173,7 +174,7 @@ func (c *Core) beginDrag(ctx context.Context, id WindowID, button uint32, resize | |||
| 173 | 174 | ||
| 174 | 175 | // dragMotion follows the cursor: a float moves or resizes, a tile shows | |
| 175 | 176 | // where it would land. | |
| 176 | - | func (c *Core) dragMotion(ctx context.Context, timeMsec uint32) error { | |
| 177 | + | func (c *Core) dragMotion(ctx context.Context, at time.Duration) error { | |
| 177 | 178 | d := c.drag | |
| 178 | 179 | _, w, ok := c.draggable(d.id) | |
| 179 | 180 | if !ok { | |
| @@ -198,7 +199,7 @@ func (c *Core) dragMotion(ctx context.Context, timeMsec uint32) error { | |||
| 198 | 199 | } | |
| 199 | 200 | return c.publish(ctx) | |
| 200 | 201 | } | |
| 201 | - | scrolled := c.edgeScroll(timeMsec) | |
| 202 | + | scrolled := c.edgeScroll(at) | |
| 202 | 203 | t := c.dropAt(d.id, c.cursorX, c.cursorY) | |
| 203 | 204 | if !scrolled && t.kind == d.target.kind && t.anchor == d.target.anchor && t.ws == d.target.ws && slices.Equal(t.hints, d.target.hints) { | |
| 204 | 205 | return nil | |
| @@ -513,9 +514,9 @@ func (c *Core) drop(id WindowID, t dropTarget) { | |||
| 513 | 514 | // edgeScroll scrolls a scroll-overflow workspace by one column when a | |
| 514 | 515 | // tile drag moves the pointer against its usable edge. It reports whether | |
| 515 | 516 | // the view moved. | |
| 516 | - | func (c *Core) edgeScroll(timeMsec uint32) bool { | |
| 517 | + | func (c *Core) edgeScroll(t time.Duration) bool { | |
| 517 | 518 | i := c.screenAt(c.cursorX, c.cursorY) | |
| 518 | - | if i < 0 || timeMsec-c.drag.scrolled < edgeScrollMsec && c.drag.scrolled != 0 { | |
| 519 | + | if i < 0 || t-c.drag.scrolled < edgeScrollInterval && c.drag.scrolled != 0 { | |
| 519 | 520 | return false | |
| 520 | 521 | } | |
| 521 | 522 | sc := c.screens[i] | |
| @@ -536,7 +537,7 @@ func (c *Core) edgeScroll(timeMsec uint32) bool { | |||
| 536 | 537 | if !w.scrollBy(dir) { | |
| 537 | 538 | return false | |
| 538 | 539 | } | |
| 539 | - | c.drag.scrolled = max(timeMsec, 1) | |
| 540 | + | c.drag.scrolled = max(t, 1) | |
| 540 | 541 | return true | |
| 541 | 542 | } | |
| 542 | 543 | ||
| @@ -3,6 +3,7 @@ package core | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "slices" | 5 | "slices" | ||
| 6 | + | "time" | |||
| 6 | 7 | ||||
| 7 | "github.com/bnema/neferwl/internal/ports" | 8 | "github.com/bnema/neferwl/internal/ports" | ||
| 8 | ) | 9 | ) | ||
| @@ -30,9 +31,9 @@ const ( | |||||
| 30 | gapPad = 12 | 31 | gapPad = 12 | ||
| 31 | // edgeScroll is the distance from the usable edge, logical px, that | 32 | // edgeScroll is the distance from the usable edge, logical px, that | ||
| 32 | // scrolls a scroll-overflow workspace during a tile drag, at most | 33 | // scrolls a scroll-overflow workspace during a tile drag, at most | ||
| 33 | - | // once per edgeScrollMsec. | 34 | + | // once per edgeScrollInterval. |
| 34 | - | edgeScroll = 32 | 35 | + | edgeScroll = 32 |
| 35 | - | edgeScrollMsec = 250 | 36 | + | edgeScrollInterval = 250 * time.Millisecond |
| 36 | ) | 37 | ) | ||
| 37 | 38 | ||||
| 38 | type dropKind int | 39 | type dropKind int | ||
| @@ -69,7 +70,7 @@ type dragState struct { | |||||
| 69 | float bool | 70 | float bool | ||
| 70 | target dropTarget | 71 | target dropTarget | ||
| 71 | // scrolled is the time of the last edge scroll. | 72 | // scrolled is the time of the last edge scroll. | ||
| 72 | - | scrolled uint32 | 73 | + | scrolled time.Duration |
| 73 | } | 74 | } | ||
| 74 | 75 | ||||
| 75 | // draggable returns the screen and workspace of a window a drag may | 76 | // draggable returns the screen and workspace of a window a drag may | ||
| @@ -173,7 +174,7 @@ func (c *Core) beginDrag(ctx context.Context, id WindowID, button uint32, resize | |||||
| 173 | 174 | ||||
| 174 | // dragMotion follows the cursor: a float moves or resizes, a tile shows | 175 | // dragMotion follows the cursor: a float moves or resizes, a tile shows | ||
| 175 | // where it would land. | 176 | // where it would land. | ||
| 176 | - | func (c *Core) dragMotion(ctx context.Context, timeMsec uint32) error { | 177 | + | func (c *Core) dragMotion(ctx context.Context, at time.Duration) error { |
| 177 | d := c.drag | 178 | d := c.drag | ||
| 178 | _, w, ok := c.draggable(d.id) | 179 | _, w, ok := c.draggable(d.id) | ||
| 179 | if !ok { | 180 | if !ok { | ||
| @@ -198,7 +199,7 @@ func (c *Core) dragMotion(ctx context.Context, timeMsec uint32) error { | |||||
| 198 | } | 199 | } | ||
| 199 | return c.publish(ctx) | 200 | return c.publish(ctx) | ||
| 200 | } | 201 | } | ||
| 201 | - | scrolled := c.edgeScroll(timeMsec) | 202 | + | scrolled := c.edgeScroll(at) |
| 202 | t := c.dropAt(d.id, c.cursorX, c.cursorY) | 203 | t := c.dropAt(d.id, c.cursorX, c.cursorY) | ||
| 203 | if !scrolled && t.kind == d.target.kind && t.anchor == d.target.anchor && t.ws == d.target.ws && slices.Equal(t.hints, d.target.hints) { | 204 | if !scrolled && t.kind == d.target.kind && t.anchor == d.target.anchor && t.ws == d.target.ws && slices.Equal(t.hints, d.target.hints) { | ||
| 204 | return nil | 205 | return nil | ||
| @@ -513,9 +514,9 @@ func (c *Core) drop(id WindowID, t dropTarget) { | |||||
| 513 | // edgeScroll scrolls a scroll-overflow workspace by one column when a | 514 | // edgeScroll scrolls a scroll-overflow workspace by one column when a | ||
| 514 | // tile drag moves the pointer against its usable edge. It reports whether | 515 | // tile drag moves the pointer against its usable edge. It reports whether | ||
| 515 | // the view moved. | 516 | // the view moved. | ||
| 516 | - | func (c *Core) edgeScroll(timeMsec uint32) bool { | 517 | + | func (c *Core) edgeScroll(t time.Duration) bool { |
| 517 | i := c.screenAt(c.cursorX, c.cursorY) | 518 | i := c.screenAt(c.cursorX, c.cursorY) | ||
| 518 | - | if i < 0 || timeMsec-c.drag.scrolled < edgeScrollMsec && c.drag.scrolled != 0 { | 519 | + | if i < 0 || t-c.drag.scrolled < edgeScrollInterval && c.drag.scrolled != 0 { |
| 519 | return false | 520 | return false | ||
| 520 | } | 521 | } | ||
| 521 | sc := c.screens[i] | 522 | sc := c.screens[i] | ||
| @@ -536,7 +537,7 @@ func (c *Core) edgeScroll(timeMsec uint32) bool { | |||||
| 536 | if !w.scrollBy(dir) { | 537 | if !w.scrollBy(dir) { | ||
| 537 | return false | 538 | return false | ||
| 538 | } | 539 | } | ||
| 539 | - | c.drag.scrolled = max(timeMsec, 1) | 540 | + | c.drag.scrolled = max(t, 1) |
| 540 | return true | 541 | return true | ||
| 541 | } | 542 | } | ||
| 542 | 543 | ||||
M internal/core/drag_test.go +1 -1
| @@ -168,7 +168,7 @@ func columnsOf(set []ports.Scene, output string) [][]ports.WindowID { | |||
| 168 | 168 | var dragOut = ports.OutputInfo{Name: "A", Width: 1000, Height: 600} | |
| 169 | 169 | ||
| 170 | 170 | func (r *dragRig) moveTo(x, y float64, msec uint32) { | |
| 171 | - | r.in(ports.PointerMotion{X: x, Y: y, TimeMsec: msec}) | |
| 171 | + | r.in(ports.PointerMotion{X: x, Y: y, Time: time.Duration(msec) * time.Millisecond}) | |
| 172 | 172 | } | |
| 173 | 173 | ||
| 174 | 174 | func (r *dragRig) press(t *testing.T, button uint32, mods ports.Mods) { | |
| @@ -168,7 +168,7 @@ func columnsOf(set []ports.Scene, output string) [][]ports.WindowID { | |||||
| 168 | var dragOut = ports.OutputInfo{Name: "A", Width: 1000, Height: 600} | 168 | var dragOut = ports.OutputInfo{Name: "A", Width: 1000, Height: 600} | ||
| 169 | 169 | ||||
| 170 | func (r *dragRig) moveTo(x, y float64, msec uint32) { | 170 | func (r *dragRig) moveTo(x, y float64, msec uint32) { | ||
| 171 | - | r.in(ports.PointerMotion{X: x, Y: y, TimeMsec: msec}) | 171 | + | r.in(ports.PointerMotion{X: x, Y: y, Time: time.Duration(msec) * time.Millisecond}) |
| 172 | } | 172 | } | ||
| 173 | 173 | ||||
| 174 | func (r *dragRig) press(t *testing.T, button uint32, mods ports.Mods) { | 174 | func (r *dragRig) press(t *testing.T, button uint32, mods ports.Mods) { | ||
M internal/core/input.go +4 -4
| @@ -18,7 +18,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 18 | 18 | } | |
| 19 | 19 | switch v := ev.(type) { | |
| 20 | 20 | case ports.PointerMotion: | |
| 21 | - | c.motionMsec = v.TimeMsec | |
| 21 | + | c.motionTime = v.Time | |
| 22 | 22 | // A locked pointer stays still; relative motion still flows. | |
| 23 | 23 | if c.constraint.Mode != ports.ConstraintLock { | |
| 24 | 24 | c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y)) | |
| @@ -40,7 +40,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 40 | 40 | } | |
| 41 | 41 | if c.drag != nil { | |
| 42 | 42 | // The dragged window and the others get no pointer events. | |
| 43 | - | return c.dragMotion(ctx, v.TimeMsec) | |
| 43 | + | return c.dragMotion(ctx, v.Time) | |
| 44 | 44 | } | |
| 45 | 45 | id, x, y := c.hit(c.cursorX, c.cursorY) | |
| 46 | 46 | // Layout changes are intentionally re-hit-tested only on motion. | |
| @@ -52,7 +52,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 52 | 52 | } | |
| 53 | 53 | c.pointerAt = [2]float64{x, y} | |
| 54 | 54 | if id != 0 { | |
| 55 | - | 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 { | |
| 55 | + | 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, Time: v.Time}); err != nil { | |
| 56 | 56 | return err | |
| 57 | 57 | } | |
| 58 | 58 | } | |
| @@ -107,7 +107,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 107 | 107 | delete(c.buttons, v.Button) | |
| 108 | 108 | } | |
| 109 | 109 | if id != 0 { | |
| 110 | - | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil { | |
| 110 | + | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil { | |
| 111 | 111 | return err | |
| 112 | 112 | } | |
| 113 | 113 | // A click on an on-demand layer gives it the keyboard. | |
| @@ -18,7 +18,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 18 | } | 18 | } | ||
| 19 | switch v := ev.(type) { | 19 | switch v := ev.(type) { | ||
| 20 | case ports.PointerMotion: | 20 | case ports.PointerMotion: | ||
| 21 | - | c.motionMsec = v.TimeMsec | 21 | + | c.motionTime = v.Time |
| 22 | // A locked pointer stays still; relative motion still flows. | 22 | // A locked pointer stays still; relative motion still flows. | ||
| 23 | if c.constraint.Mode != ports.ConstraintLock { | 23 | if c.constraint.Mode != ports.ConstraintLock { | ||
| 24 | c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y)) | 24 | c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y)) | ||
| @@ -40,7 +40,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 40 | } | 40 | } | ||
| 41 | if c.drag != nil { | 41 | if c.drag != nil { | ||
| 42 | // The dragged window and the others get no pointer events. | 42 | // The dragged window and the others get no pointer events. | ||
| 43 | - | return c.dragMotion(ctx, v.TimeMsec) | 43 | + | return c.dragMotion(ctx, v.Time) |
| 44 | } | 44 | } | ||
| 45 | id, x, y := c.hit(c.cursorX, c.cursorY) | 45 | id, x, y := c.hit(c.cursorX, c.cursorY) | ||
| 46 | // Layout changes are intentionally re-hit-tested only on motion. | 46 | // Layout changes are intentionally re-hit-tested only on motion. | ||
| @@ -52,7 +52,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 52 | } | 52 | } | ||
| 53 | c.pointerAt = [2]float64{x, y} | 53 | c.pointerAt = [2]float64{x, y} | ||
| 54 | if id != 0 { | 54 | if id != 0 { | ||
| 55 | - | 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 { | 55 | + | 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, Time: v.Time}); err != nil { |
| 56 | return err | 56 | return err | ||
| 57 | } | 57 | } | ||
| 58 | } | 58 | } | ||
| @@ -107,7 +107,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 107 | delete(c.buttons, v.Button) | 107 | delete(c.buttons, v.Button) | ||
| 108 | } | 108 | } | ||
| 109 | if id != 0 { | 109 | if id != 0 { | ||
| 110 | - | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil { | 110 | + | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil { |
| 111 | return err | 111 | return err | ||
| 112 | } | 112 | } | ||
| 113 | // A click on an on-demand layer gives it the keyboard. | 113 | // A click on an on-demand layer gives it the keyboard. | ||
M internal/core/security.go +4 -4
| @@ -25,7 +25,7 @@ func (c *Core) syncSecurity() bool { | |||
| 25 | 25 | c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{} | |
| 26 | 26 | c.pointer, c.grab, c.pointerOutput = 0, 0, "" | |
| 27 | 27 | c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 0 | |
| 28 | - | c.pointerAt, c.motionMsec = [2]float64{}, 0 | |
| 28 | + | c.pointerAt, c.motionTime = [2]float64{}, 0 | |
| 29 | 29 | c.keyboard = keyboard{} | |
| 30 | 30 | c.constrained, c.constraint = ports.PointerConstrained{}, ports.PointerConstraint{} | |
| 31 | 31 | if c.ch.Constraints != nil { | |
| @@ -249,7 +249,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 249 | 249 | return c.command(ctx, ports.ForwardKey{ID: id, Key: v}) | |
| 250 | 250 | } | |
| 251 | 251 | case ports.PointerMotion: | |
| 252 | - | c.motionMsec = v.TimeMsec | |
| 252 | + | c.motionTime = v.Time | |
| 253 | 253 | c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y) | |
| 254 | 254 | id, x, y := c.lockHit(c.cursorX, c.cursorY) | |
| 255 | 255 | if id != c.pointer { | |
| @@ -260,7 +260,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 260 | 260 | } | |
| 261 | 261 | c.pointerAt = [2]float64{x, y} | |
| 262 | 262 | if id != 0 { | |
| 263 | - | return 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}) | |
| 263 | + | return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, Time: v.Time}) | |
| 264 | 264 | } | |
| 265 | 265 | case ports.PointerButton: | |
| 266 | 266 | id := c.pointer | |
| @@ -288,7 +288,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 288 | 288 | } | |
| 289 | 289 | } | |
| 290 | 290 | } | |
| 291 | - | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil { | |
| 291 | + | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil { | |
| 292 | 292 | return err | |
| 293 | 293 | } | |
| 294 | 294 | } | |
| @@ -25,7 +25,7 @@ func (c *Core) syncSecurity() bool { | |||||
| 25 | c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{} | 25 | c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{} | ||
| 26 | c.pointer, c.grab, c.pointerOutput = 0, 0, "" | 26 | c.pointer, c.grab, c.pointerOutput = 0, 0, "" | ||
| 27 | c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 0 | 27 | c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 0 | ||
| 28 | - | c.pointerAt, c.motionMsec = [2]float64{}, 0 | 28 | + | c.pointerAt, c.motionTime = [2]float64{}, 0 |
| 29 | c.keyboard = keyboard{} | 29 | c.keyboard = keyboard{} | ||
| 30 | c.constrained, c.constraint = ports.PointerConstrained{}, ports.PointerConstraint{} | 30 | c.constrained, c.constraint = ports.PointerConstrained{}, ports.PointerConstraint{} | ||
| 31 | if c.ch.Constraints != nil { | 31 | if c.ch.Constraints != nil { | ||
| @@ -249,7 +249,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 249 | return c.command(ctx, ports.ForwardKey{ID: id, Key: v}) | 249 | return c.command(ctx, ports.ForwardKey{ID: id, Key: v}) | ||
| 250 | } | 250 | } | ||
| 251 | case ports.PointerMotion: | 251 | case ports.PointerMotion: | ||
| 252 | - | c.motionMsec = v.TimeMsec | 252 | + | c.motionTime = v.Time |
| 253 | c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y) | 253 | c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y) | ||
| 254 | id, x, y := c.lockHit(c.cursorX, c.cursorY) | 254 | id, x, y := c.lockHit(c.cursorX, c.cursorY) | ||
| 255 | if id != c.pointer { | 255 | if id != c.pointer { | ||
| @@ -260,7 +260,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 260 | } | 260 | } | ||
| 261 | c.pointerAt = [2]float64{x, y} | 261 | c.pointerAt = [2]float64{x, y} | ||
| 262 | if id != 0 { | 262 | if id != 0 { | ||
| 263 | - | return 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}) | 263 | + | return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, Time: v.Time}) |
| 264 | } | 264 | } | ||
| 265 | case ports.PointerButton: | 265 | case ports.PointerButton: | ||
| 266 | id := c.pointer | 266 | id := c.pointer | ||
| @@ -288,7 +288,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 288 | } | 288 | } | ||
| 289 | } | 289 | } | ||
| 290 | } | 290 | } | ||
| 291 | - | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil { | 291 | + | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil { |
| 292 | return err | 292 | return err | ||
| 293 | } | 293 | } | ||
| 294 | } | 294 | } | ||
M internal/ports/client_commands.go +8 -7
| @@ -161,24 +161,25 @@ type PointerFocus struct { | |||
| 161 | 161 | func (PointerFocus) clientCommand() {} | |
| 162 | 162 | ||
| 163 | 163 | // PointerMotionTo carries core → wayland motion for the pointer focus, in | |
| 164 | - | // window coordinates. The deltas and TimeUsec feed zwp_relative_pointer_v1; | |
| 164 | + | // window coordinates. The deltas and Time feed zwp_relative_pointer_v1; | |
| 165 | 165 | // While the window locks the pointer, wayland sends only relative motion. | |
| 166 | 166 | type PointerMotionTo struct { | |
| 167 | 167 | ID WindowID | |
| 168 | 168 | X, Y float64 | |
| 169 | 169 | DX, DY float64 | |
| 170 | 170 | UnaccelDX, UnaccelDY float64 | |
| 171 | - | TimeMsec uint32 | |
| 172 | - | TimeUsec uint64 | |
| 171 | + | // Time is the device timestamp (CLOCK_MONOTONIC). | |
| 172 | + | Time time.Duration | |
| 173 | 173 | } | |
| 174 | 174 | ||
| 175 | 175 | func (PointerMotionTo) clientCommand() {} | |
| 176 | 176 | ||
| 177 | 177 | type PointerButtonTo struct { | |
| 178 | - | ID WindowID | |
| 179 | - | Button uint32 | |
| 180 | - | Pressed bool | |
| 181 | - | TimeMsec uint32 | |
| 178 | + | ID WindowID | |
| 179 | + | Button uint32 | |
| 180 | + | Pressed bool | |
| 181 | + | // Time is the device timestamp (CLOCK_MONOTONIC). | |
| 182 | + | Time time.Duration | |
| 182 | 183 | } | |
| 183 | 184 | ||
| 184 | 185 | func (PointerButtonTo) clientCommand() {} | |
| @@ -161,24 +161,25 @@ type PointerFocus struct { | |||||
| 161 | func (PointerFocus) clientCommand() {} | 161 | func (PointerFocus) clientCommand() {} | ||
| 162 | 162 | ||||
| 163 | // PointerMotionTo carries core → wayland motion for the pointer focus, in | 163 | // PointerMotionTo carries core → wayland motion for the pointer focus, in | ||
| 164 | - | // window coordinates. The deltas and TimeUsec feed zwp_relative_pointer_v1; | 164 | + | // window coordinates. The deltas and Time feed zwp_relative_pointer_v1; |
| 165 | // While the window locks the pointer, wayland sends only relative motion. | 165 | // While the window locks the pointer, wayland sends only relative motion. | ||
| 166 | type PointerMotionTo struct { | 166 | type PointerMotionTo struct { | ||
| 167 | ID WindowID | 167 | ID WindowID | ||
| 168 | X, Y float64 | 168 | X, Y float64 | ||
| 169 | DX, DY float64 | 169 | DX, DY float64 | ||
| 170 | UnaccelDX, UnaccelDY float64 | 170 | UnaccelDX, UnaccelDY float64 | ||
| 171 | - | TimeMsec uint32 | 171 | + | // Time is the device timestamp (CLOCK_MONOTONIC). |
| 172 | - | TimeUsec uint64 | 172 | + | Time time.Duration |
| 173 | } | 173 | } | ||
| 174 | 174 | ||||
| 175 | func (PointerMotionTo) clientCommand() {} | 175 | func (PointerMotionTo) clientCommand() {} | ||
| 176 | 176 | ||||
| 177 | type PointerButtonTo struct { | 177 | type PointerButtonTo struct { | ||
| 178 | - | ID WindowID | 178 | + | ID WindowID |
| 179 | - | Button uint32 | 179 | + | Button uint32 |
| 180 | - | Pressed bool | 180 | + | Pressed bool |
| 181 | - | TimeMsec uint32 | 181 | + | // Time is the device timestamp (CLOCK_MONOTONIC). |
| 182 | + | Time time.Duration | |||
| 182 | } | 183 | } | ||
| 183 | 184 | ||||
| 184 | func (PointerButtonTo) clientCommand() {} | 185 | func (PointerButtonTo) clientCommand() {} | ||
M internal/ports/input_events.go +19 -17
| @@ -17,17 +17,18 @@ type InputEvent interface{ inputEvent() } | |||
| 17 | 17 | ||
| 18 | 18 | // KeyEvent carries input → core key transitions; Keysym is an xkb keysym name. | |
| 19 | 19 | // Keycode is the evdev code (xkb keycode - 8) and State the xkb modifier state | |
| 20 | - | // after this transition; wayland forwards both to clients unchanged. | |
| 20 | + | // after this transition; wayland forwards both to clients unchanged. Time is | |
| 21 | + | // the device timestamp (CLOCK_MONOTONIC), like SwipeBegin.Time. | |
| 21 | 22 | type KeyEvent struct { | |
| 22 | 23 | Keysym string | |
| 23 | 24 | // Base is the key's unshifted keysym in the active layout, so Cmd+Shift+1 | |
| 24 | 25 | // still matches a cmd+shift+1 bind although it prints exclam. | |
| 25 | - | Base string | |
| 26 | - | Mods Mods | |
| 27 | - | Pressed bool | |
| 28 | - | TimeMsec uint32 | |
| 29 | - | Keycode uint32 | |
| 30 | - | State ModState | |
| 26 | + | Base string | |
| 27 | + | Mods Mods | |
| 28 | + | Pressed bool | |
| 29 | + | Time time.Duration | |
| 30 | + | Keycode uint32 | |
| 31 | + | State ModState | |
| 31 | 32 | } | |
| 32 | 33 | ||
| 33 | 34 | // ModState is the serialized xkb modifier state sent in wl_keyboard.modifiers. | |
| @@ -37,23 +38,23 @@ func (KeyEvent) inputEvent() {} | |||
| 37 | 38 | ||
| 38 | 39 | // PointerMotion uses global layout coordinates in logical pixels (see OutputPlacement). | |
| 39 | 40 | // DX and DY are the accelerated logical deltas and UnaccelDX, UnaccelDY the | |
| 40 | - | // raw device deltas; all four are 0 for absolute devices. TimeUsec is the | |
| 41 | - | // device timestamp in microseconds. | |
| 41 | + | // raw device deltas; all four are 0 for absolute devices. Time is the device | |
| 42 | + | // timestamp (CLOCK_MONOTONIC). | |
| 42 | 43 | type PointerMotion struct { | |
| 43 | 44 | X, Y float64 | |
| 44 | 45 | DX, DY float64 | |
| 45 | 46 | UnaccelDX, UnaccelDY float64 | |
| 46 | - | TimeMsec uint32 | |
| 47 | - | TimeUsec uint64 | |
| 47 | + | Time time.Duration | |
| 48 | 48 | } | |
| 49 | 49 | ||
| 50 | 50 | func (PointerMotion) inputEvent() {} | |
| 51 | 51 | ||
| 52 | - | // PointerButton uses evdev button codes (BTN_LEFT is 0x110). | |
| 52 | + | // PointerButton uses evdev button codes (BTN_LEFT is 0x110). Time is the | |
| 53 | + | // device timestamp (CLOCK_MONOTONIC). | |
| 53 | 54 | type PointerButton struct { | |
| 54 | - | Button uint32 | |
| 55 | - | Pressed bool | |
| 56 | - | TimeMsec uint32 | |
| 55 | + | Button uint32 | |
| 56 | + | Pressed bool | |
| 57 | + | Time time.Duration | |
| 57 | 58 | } | |
| 58 | 59 | ||
| 59 | 60 | func (PointerButton) inputEvent() {} | |
| @@ -77,11 +78,12 @@ type ScrollAxis struct { | |||
| 77 | 78 | Stop bool | |
| 78 | 79 | } | |
| 79 | 80 | ||
| 80 | - | // PointerAxis is one scroll frame: Vertical, then Horizontal. | |
| 81 | + | // PointerAxis is one scroll frame: Vertical, then Horizontal. Time is the | |
| 82 | + | // device timestamp (CLOCK_MONOTONIC). | |
| 81 | 83 | type PointerAxis struct { | |
| 82 | 84 | Source AxisSource | |
| 83 | 85 | Vertical, Horizontal ScrollAxis | |
| 84 | - | TimeMsec uint32 | |
| 86 | + | Time time.Duration | |
| 85 | 87 | } | |
| 86 | 88 | ||
| 87 | 89 | func (PointerAxis) inputEvent() {} | |
| @@ -17,17 +17,18 @@ type InputEvent interface{ inputEvent() } | |||||
| 17 | 17 | ||||
| 18 | // KeyEvent carries input → core key transitions; Keysym is an xkb keysym name. | 18 | // KeyEvent carries input → core key transitions; Keysym is an xkb keysym name. | ||
| 19 | // Keycode is the evdev code (xkb keycode - 8) and State the xkb modifier state | 19 | // Keycode is the evdev code (xkb keycode - 8) and State the xkb modifier state | ||
| 20 | - | // after this transition; wayland forwards both to clients unchanged. | 20 | + | // after this transition; wayland forwards both to clients unchanged. Time is |
| 21 | + | // the device timestamp (CLOCK_MONOTONIC), like SwipeBegin.Time. | |||
| 21 | type KeyEvent struct { | 22 | type KeyEvent struct { | ||
| 22 | Keysym string | 23 | Keysym string | ||
| 23 | // Base is the key's unshifted keysym in the active layout, so Cmd+Shift+1 | 24 | // Base is the key's unshifted keysym in the active layout, so Cmd+Shift+1 | ||
| 24 | // still matches a cmd+shift+1 bind although it prints exclam. | 25 | // still matches a cmd+shift+1 bind although it prints exclam. | ||
| 25 | - | Base string | 26 | + | Base string |
| 26 | - | Mods Mods | 27 | + | Mods Mods |
| 27 | - | Pressed bool | 28 | + | Pressed bool |
| 28 | - | TimeMsec uint32 | 29 | + | Time time.Duration |
| 29 | - | Keycode uint32 | 30 | + | Keycode uint32 |
| 30 | - | State ModState | 31 | + | State ModState |
| 31 | } | 32 | } | ||
| 32 | 33 | ||||
| 33 | // ModState is the serialized xkb modifier state sent in wl_keyboard.modifiers. | 34 | // ModState is the serialized xkb modifier state sent in wl_keyboard.modifiers. | ||
| @@ -37,23 +38,23 @@ func (KeyEvent) inputEvent() {} | |||||
| 37 | 38 | ||||
| 38 | // PointerMotion uses global layout coordinates in logical pixels (see OutputPlacement). | 39 | // PointerMotion uses global layout coordinates in logical pixels (see OutputPlacement). | ||
| 39 | // DX and DY are the accelerated logical deltas and UnaccelDX, UnaccelDY the | 40 | // DX and DY are the accelerated logical deltas and UnaccelDX, UnaccelDY the | ||
| 40 | - | // raw device deltas; all four are 0 for absolute devices. TimeUsec is the | 41 | + | // raw device deltas; all four are 0 for absolute devices. Time is the device |
| 41 | - | // device timestamp in microseconds. | 42 | + | // timestamp (CLOCK_MONOTONIC). |
| 42 | type PointerMotion struct { | 43 | type PointerMotion struct { | ||
| 43 | X, Y float64 | 44 | X, Y float64 | ||
| 44 | DX, DY float64 | 45 | DX, DY float64 | ||
| 45 | UnaccelDX, UnaccelDY float64 | 46 | UnaccelDX, UnaccelDY float64 | ||
| 46 | - | TimeMsec uint32 | 47 | + | Time time.Duration |
| 47 | - | TimeUsec uint64 | |||
| 48 | } | 48 | } | ||
| 49 | 49 | ||||
| 50 | func (PointerMotion) inputEvent() {} | 50 | func (PointerMotion) inputEvent() {} | ||
| 51 | 51 | ||||
| 52 | - | // PointerButton uses evdev button codes (BTN_LEFT is 0x110). | 52 | + | // PointerButton uses evdev button codes (BTN_LEFT is 0x110). Time is the |
| 53 | + | // device timestamp (CLOCK_MONOTONIC). | |||
| 53 | type PointerButton struct { | 54 | type PointerButton struct { | ||
| 54 | - | Button uint32 | 55 | + | Button uint32 |
| 55 | - | Pressed bool | 56 | + | Pressed bool |
| 56 | - | TimeMsec uint32 | 57 | + | Time time.Duration |
| 57 | } | 58 | } | ||
| 58 | 59 | ||||
| 59 | func (PointerButton) inputEvent() {} | 60 | func (PointerButton) inputEvent() {} | ||
| @@ -77,11 +78,12 @@ type ScrollAxis struct { | |||||
| 77 | Stop bool | 78 | Stop bool | ||
| 78 | } | 79 | } | ||
| 79 | 80 | ||||
| 80 | - | // PointerAxis is one scroll frame: Vertical, then Horizontal. | 81 | + | // PointerAxis is one scroll frame: Vertical, then Horizontal. Time is the |
| 82 | + | // device timestamp (CLOCK_MONOTONIC). | |||
| 81 | type PointerAxis struct { | 83 | type PointerAxis struct { | ||
| 82 | Source AxisSource | 84 | Source AxisSource | ||
| 83 | Vertical, Horizontal ScrollAxis | 85 | Vertical, Horizontal ScrollAxis | ||
| 84 | - | TimeMsec uint32 | 86 | + | Time time.Duration |
| 85 | } | 87 | } | ||
| 86 | 88 | ||||
| 87 | func (PointerAxis) inputEvent() {} | 89 | func (PointerAxis) inputEvent() {} | ||