mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-20 23:59:55 +00:00
Add ExtendedMouseButtons for back/forward mouse buttons
This commit is contained in:
@@ -272,15 +272,15 @@ func ensureEventSource() uintptr {
|
||||
|
||||
// MacInputInjector injects keyboard and mouse events via Core Graphics.
|
||||
type MacInputInjector struct {
|
||||
lastButtons uint8
|
||||
lastButtons uint16
|
||||
pbcopyPath string
|
||||
pbpastePath string
|
||||
// clickCount[i] / clickAt[i] track the multi-click sequence for
|
||||
// button i (0=left, 1=right, 2=middle). macOS apps reconstruct
|
||||
// double/triple click semantics from the kCGMouseEventClickState
|
||||
// field on each posted event, not from event timing.
|
||||
clickCount [3]int64
|
||||
clickAt [3]time.Time
|
||||
clickCount [5]int64
|
||||
clickAt [5]time.Time
|
||||
}
|
||||
|
||||
// NewMacInputInjector creates a macOS input injector.
|
||||
@@ -406,7 +406,7 @@ func (m *MacInputInjector) postMacKey(src uintptr, keycode uint16, down bool) {
|
||||
}
|
||||
|
||||
// InjectPointer simulates mouse movement and button events.
|
||||
func (m *MacInputInjector) InjectPointer(buttonMask uint8, px, py, serverW, serverH int) {
|
||||
func (m *MacInputInjector) InjectPointer(buttonMask uint16, px, py, serverW, serverH int) {
|
||||
wakeDisplay()
|
||||
if serverW == 0 || serverH == 0 {
|
||||
return
|
||||
@@ -438,7 +438,7 @@ func scalePxToLogical(px, py, serverW, serverH int) (float64, float64) {
|
||||
float64(py) * float64(logicalH) / float64(serverH)
|
||||
}
|
||||
|
||||
func (m *MacInputInjector) dispatchPointer(src uintptr, buttonMask uint8, x, y float64) {
|
||||
func (m *MacInputInjector) dispatchPointer(src uintptr, buttonMask uint16, x, y float64) {
|
||||
leftDown := buttonMask&0x01 != 0
|
||||
rightDown := buttonMask&0x04 != 0
|
||||
middleDown := buttonMask&0x02 != 0
|
||||
@@ -462,8 +462,8 @@ func (m *MacInputInjector) postMoveOrDrag(src uintptr, leftDown, rightDown bool,
|
||||
// postButtonTransitions emits the up/down events for each button whose
|
||||
// state changed against m.lastButtons, computing the click count so
|
||||
// macOS recognises double / triple clicks.
|
||||
func (m *MacInputInjector) postButtonTransitions(src uintptr, buttonMask uint8, x, y float64) {
|
||||
emit := func(curBit, prevBit uint8, down, up int32, button int32, idx int) {
|
||||
func (m *MacInputInjector) postButtonTransitions(src uintptr, buttonMask uint16, x, y float64) {
|
||||
emit := func(curBit, prevBit uint16, down, up int32, button int32, idx int) {
|
||||
cur := buttonMask&curBit != 0
|
||||
prev := m.lastButtons&prevBit != 0
|
||||
if cur && !prev {
|
||||
@@ -486,9 +486,14 @@ func (m *MacInputInjector) postButtonTransitions(src uintptr, buttonMask uint8,
|
||||
emit(0x01, 0x01, kCGEventLeftMouseDown, kCGEventLeftMouseUp, kCGMouseButtonLeft, 0)
|
||||
emit(0x04, 0x04, kCGEventRightMouseDown, kCGEventRightMouseUp, kCGMouseButtonRight, 1)
|
||||
emit(0x02, 0x02, kCGEventOtherMouseDown, kCGEventOtherMouseUp, kCGMouseButtonCenter, 2)
|
||||
// CG mouse-button numbers 3 (back) and 4 (forward) are emitted as
|
||||
// "other" events; macOS apps that swallow Browser nav (Finder, web
|
||||
// views) react to these directly.
|
||||
emit(1<<7, 1<<7, kCGEventOtherMouseDown, kCGEventOtherMouseUp, 3, 3)
|
||||
emit(1<<8, 1<<8, kCGEventOtherMouseDown, kCGEventOtherMouseUp, 4, 4)
|
||||
}
|
||||
|
||||
func (m *MacInputInjector) postScrollWheel(src uintptr, buttonMask uint8) {
|
||||
func (m *MacInputInjector) postScrollWheel(src uintptr, buttonMask uint16) {
|
||||
if buttonMask&0x08 != 0 {
|
||||
m.postScroll(src, scrollPixelsPerWheelTick)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ const (
|
||||
btnLeft = 0x110
|
||||
btnRight = 0x111
|
||||
btnMiddle = 0x112
|
||||
btnSide = 0x113 // mouse-back (X1)
|
||||
btnExtra = 0x114 // mouse-forward (X2)
|
||||
)
|
||||
|
||||
// inputEvent matches struct input_event for x86_64 (timeval is 16 bytes).
|
||||
@@ -63,7 +65,7 @@ type UInputInjector struct {
|
||||
fd int
|
||||
closeOnce sync.Once
|
||||
keysymToKey map[uint32]uint16
|
||||
prevButtons uint8
|
||||
prevButtons uint16
|
||||
screenW int
|
||||
screenH int
|
||||
}
|
||||
@@ -233,7 +235,7 @@ func (u *UInputInjector) emitKeyCode(code uint16, down bool) {
|
||||
|
||||
// InjectPointer moves the absolute pointer and presses/releases buttons
|
||||
// based on the RFB button mask delta against the previous mask.
|
||||
func (u *UInputInjector) InjectPointer(buttonMask uint8, x, y, serverW, serverH int) {
|
||||
func (u *UInputInjector) InjectPointer(buttonMask uint16, x, y, serverW, serverH int) {
|
||||
u.mu.Lock()
|
||||
defer u.mu.Unlock()
|
||||
if serverW <= 1 || serverH <= 1 {
|
||||
@@ -245,13 +247,15 @@ func (u *UInputInjector) InjectPointer(buttonMask uint8, x, y, serverW, serverH
|
||||
_ = u.emit(evAbs, absY, absYVal)
|
||||
|
||||
type btnMap struct {
|
||||
bit uint8
|
||||
bit uint16
|
||||
key uint16
|
||||
}
|
||||
for _, b := range []btnMap{
|
||||
{0x01, btnLeft},
|
||||
{0x02, btnMiddle},
|
||||
{0x04, btnRight},
|
||||
{1 << 7, btnSide},
|
||||
{1 << 8, btnExtra},
|
||||
} {
|
||||
pressed := buttonMask&b.bit != 0
|
||||
was := u.prevButtons&b.bit != 0
|
||||
|
||||
@@ -30,9 +30,16 @@ const (
|
||||
mouseeventfRightUp = 0x0010
|
||||
mouseeventfMiddleDown = 0x0020
|
||||
mouseeventfMiddleUp = 0x0040
|
||||
mouseeventfXDown = 0x0080
|
||||
mouseeventfXUp = 0x0100
|
||||
mouseeventfWheel = 0x0800
|
||||
mouseeventfAbsolute = 0x8000
|
||||
|
||||
// X-button identifiers carried in the dwData field of MOUSEEVENTF_X*
|
||||
// events. XBUTTON1 is mouse-back, XBUTTON2 is mouse-forward.
|
||||
xButton1 = 0x0001
|
||||
xButton2 = 0x0002
|
||||
|
||||
wheelDelta = 120
|
||||
|
||||
keyeventfExtendedKey = 0x0001
|
||||
@@ -112,7 +119,7 @@ type inputCmd struct {
|
||||
keysym uint32
|
||||
scancode uint32
|
||||
down bool
|
||||
buttonMask uint8
|
||||
buttonMask uint16
|
||||
x, y int
|
||||
serverW int
|
||||
serverH int
|
||||
@@ -127,7 +134,7 @@ type WindowsInputInjector struct {
|
||||
ch chan inputCmd
|
||||
closed chan struct{}
|
||||
closeOnce sync.Once
|
||||
prevButtonMask uint8
|
||||
prevButtonMask uint16
|
||||
ctrlDown bool
|
||||
altDown bool
|
||||
}
|
||||
@@ -220,7 +227,7 @@ func (w *WindowsInputInjector) InjectKeyScancode(scancode uint32, keysym uint32,
|
||||
// thread. Pointer events coalesce: when the channel is full (slow desktop
|
||||
// switch, hung SendInput), drop the new sample so the read loop never
|
||||
// blocks. The next mouse event carries fresher position anyway.
|
||||
func (w *WindowsInputInjector) InjectPointer(buttonMask uint8, x, y, serverW, serverH int) {
|
||||
func (w *WindowsInputInjector) InjectPointer(buttonMask uint16, x, y, serverW, serverH int) {
|
||||
w.tryEnqueue(inputCmd{buttonMask: buttonMask, x: x, y: y, serverW: serverW, serverH: serverH})
|
||||
}
|
||||
|
||||
@@ -303,7 +310,7 @@ func signalSAS() {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WindowsInputInjector) doInjectPointer(buttonMask uint8, x, y, serverW, serverH int) {
|
||||
func (w *WindowsInputInjector) doInjectPointer(buttonMask uint16, x, y, serverW, serverH int) {
|
||||
if serverW == 0 || serverH == 0 {
|
||||
return
|
||||
}
|
||||
@@ -317,7 +324,7 @@ func (w *WindowsInputInjector) doInjectPointer(buttonMask uint8, x, y, serverW,
|
||||
w.prevButtonMask = buttonMask
|
||||
|
||||
type btnMap struct {
|
||||
bit uint8
|
||||
bit uint16
|
||||
down uint32
|
||||
up uint32
|
||||
}
|
||||
@@ -346,6 +353,26 @@ func (w *WindowsInputInjector) doInjectPointer(buttonMask uint8, x, y, serverW,
|
||||
if changed&0x10 != 0 && buttonMask&0x10 != 0 {
|
||||
sendMouseInput(mouseeventfWheel|mouseeventfAbsolute, absX, absY, negWheelDelta)
|
||||
}
|
||||
|
||||
// XBUTTON1/back at bit 7, XBUTTON2/forward at bit 8. SendInput
|
||||
// MOUSEEVENTF_X{DOWN,UP} carries the X button number in dwData.
|
||||
xbuttons := [...]struct {
|
||||
bit uint16
|
||||
data uint32
|
||||
}{
|
||||
{1 << 7, xButton1},
|
||||
{1 << 8, xButton2},
|
||||
}
|
||||
for _, b := range xbuttons {
|
||||
if changed&b.bit == 0 {
|
||||
continue
|
||||
}
|
||||
var flags uint32 = mouseeventfXUp
|
||||
if buttonMask&b.bit != 0 {
|
||||
flags = mouseeventfXDown
|
||||
}
|
||||
sendMouseInput(flags|mouseeventfAbsolute, absX, absY, b.data)
|
||||
}
|
||||
}
|
||||
|
||||
// keysym2VK converts an X11 keysym to a Windows virtual key code.
|
||||
|
||||
@@ -22,7 +22,7 @@ type X11InputInjector struct {
|
||||
screen *xproto.ScreenInfo
|
||||
display string
|
||||
keysymMap map[uint32]byte
|
||||
lastButtons uint8
|
||||
lastButtons uint16
|
||||
clipboardTool string
|
||||
clipboardToolName string
|
||||
}
|
||||
@@ -110,7 +110,7 @@ func (x *X11InputInjector) fakeKeyEvent(keycode byte, down bool) {
|
||||
}
|
||||
|
||||
// InjectPointer simulates mouse movement and button events.
|
||||
func (x *X11InputInjector) InjectPointer(buttonMask uint8, px, py, serverW, serverH int) {
|
||||
func (x *X11InputInjector) InjectPointer(buttonMask uint16, px, py, serverW, serverH int) {
|
||||
if serverW == 0 || serverH == 0 {
|
||||
return
|
||||
}
|
||||
@@ -128,15 +128,19 @@ func (x *X11InputInjector) InjectPointer(buttonMask uint8, px, py, serverW, serv
|
||||
// bit3=scrollUp, bit4=scrollDown. X11 buttons: 1=left, 2=middle, 3=right,
|
||||
// 4=scrollUp, 5=scrollDown.
|
||||
type btnMap struct {
|
||||
rfbBit uint8
|
||||
rfbBit uint16
|
||||
x11Btn byte
|
||||
}
|
||||
// X11 button numbers: 1=left, 2=middle, 3=right, 4/5=scroll up/down,
|
||||
// 6/7=scroll left/right (skipped), 8=back, 9=forward.
|
||||
buttons := [...]btnMap{
|
||||
{0x01, 1}, // left
|
||||
{0x02, 2}, // middle
|
||||
{0x04, 3}, // right
|
||||
{0x08, 4}, // scroll up
|
||||
{0x10, 5}, // scroll down
|
||||
{0x01, 1},
|
||||
{0x02, 2},
|
||||
{0x04, 3},
|
||||
{0x08, 4},
|
||||
{0x10, 5},
|
||||
{1 << 7, 8},
|
||||
{1 << 8, 9},
|
||||
}
|
||||
|
||||
for _, b := range buttons {
|
||||
|
||||
@@ -77,6 +77,7 @@ const (
|
||||
pseudoEncQEMUExtendedKeyEvent = -258
|
||||
pseudoEncDesktopName = -307
|
||||
pseudoEncExtendedDesktopSize = -308
|
||||
pseudoEncExtendedMouseButtons = -316
|
||||
|
||||
// Quality/Compression level pseudo-encodings. The client picks one
|
||||
// value from each range to tune JPEG quality and zlib effort. 0 is
|
||||
|
||||
@@ -105,7 +105,10 @@ type InputInjector interface {
|
||||
// for the given code; that's strictly no worse than the legacy path.
|
||||
InjectKeyScancode(scancode uint32, keysym uint32, down bool)
|
||||
// InjectPointer simulates mouse movement and button state.
|
||||
InjectPointer(buttonMask uint8, x, y, serverW, serverH int)
|
||||
// buttonMask is the RFB ExtendedMouseButtons mask: bits 0-6 follow
|
||||
// the standard PointerEvent layout (left/middle/right/wheel),
|
||||
// bit 7 is mouse-back (X1), bit 8 is mouse-forward (X2).
|
||||
InjectPointer(buttonMask uint16, x, y, serverW, serverH int)
|
||||
// SetClipboard sets the system clipboard to the given text.
|
||||
SetClipboard(text string)
|
||||
// GetClipboard returns the current system clipboard text.
|
||||
|
||||
@@ -79,7 +79,18 @@ type session struct {
|
||||
clientSupportsQEMUKey bool
|
||||
clientSupportsExtClipboard bool
|
||||
clientSupportsCursor bool
|
||||
extClipCapsSent bool
|
||||
// clientSupportsExtMouseButtons is set when the client advertises the
|
||||
// ExtendedMouseButtons pseudo-encoding (-316). Once the server emits
|
||||
// the ack rect, the client switches its pointer events to the 6-byte
|
||||
// extended format that carries back/forward buttons in a second mask
|
||||
// byte. Without this gate the byte after the type field would still
|
||||
// be a standard 7-bit mask and our parser must not look further.
|
||||
clientSupportsExtMouseButtons bool
|
||||
// extMouseAckSent is set once we've emitted the pseudo-rect ack that
|
||||
// flips the client into extended-pointer mode. Sticky for the
|
||||
// session because the client only needs to see it once.
|
||||
extMouseAckSent bool
|
||||
extClipCapsSent bool
|
||||
// lastCursorSerial is the serial of the cursor sprite last emitted.
|
||||
// The encoder re-queries the source each cycle and only emits when
|
||||
// the serial changes.
|
||||
@@ -359,6 +370,10 @@ func (s *session) handleSetEncodings() error {
|
||||
if sendExtClipCaps {
|
||||
s.extClipCapsSent = true
|
||||
}
|
||||
sendExtMouseAck := s.clientSupportsExtMouseButtons && !s.extMouseAckSent
|
||||
if sendExtMouseAck {
|
||||
s.extMouseAckSent = true
|
||||
}
|
||||
s.encMu.Unlock()
|
||||
if len(encs) > 0 {
|
||||
s.log.Debugf("client supports encodings: %s", strings.Join(encs, ", "))
|
||||
@@ -368,6 +383,11 @@ func (s *session) handleSetEncodings() error {
|
||||
return fmt.Errorf("send ext clipboard caps: %w", err)
|
||||
}
|
||||
}
|
||||
if sendExtMouseAck {
|
||||
if err := s.sendExtMouseAck(); err != nil {
|
||||
return fmt.Errorf("send ext mouse ack: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -387,6 +407,7 @@ func (s *session) resetEncodingCaps() {
|
||||
s.clientSupportsQEMUKey = false
|
||||
s.clientSupportsExtClipboard = false
|
||||
s.clientSupportsCursor = false
|
||||
s.clientSupportsExtMouseButtons = false
|
||||
s.cursorSourceFailed = false
|
||||
s.clientJPEGQuality = -1
|
||||
s.clientZlibLevel = -1
|
||||
@@ -427,6 +448,9 @@ func (s *session) applyEncoding(enc int32) string {
|
||||
}
|
||||
s.clientSupportsCursor = true
|
||||
return "cursor"
|
||||
case pseudoEncExtendedMouseButtons:
|
||||
s.clientSupportsExtMouseButtons = true
|
||||
return "ext-mouse-buttons"
|
||||
case encTight:
|
||||
s.useTight = true
|
||||
return "tight"
|
||||
@@ -541,14 +565,28 @@ func (s *session) handlePointerEvent() error {
|
||||
if _, err := io.ReadFull(s.conn, data[:]); err != nil {
|
||||
return fmt.Errorf("read PointerEvent: %w", err)
|
||||
}
|
||||
buttonMask := data[0]
|
||||
mask := uint16(data[0])
|
||||
x := int(binary.BigEndian.Uint16(data[1:3]))
|
||||
y := int(binary.BigEndian.Uint16(data[3:5]))
|
||||
|
||||
s.encMu.RLock()
|
||||
extended := s.clientSupportsExtMouseButtons && s.extMouseAckSent
|
||||
s.encMu.RUnlock()
|
||||
if extended && mask&0x80 != 0 {
|
||||
var hi [1]byte
|
||||
if _, err := io.ReadFull(s.conn, hi[:]); err != nil {
|
||||
return fmt.Errorf("read ExtendedPointerEvent tail: %w", err)
|
||||
}
|
||||
// Strip the marker bit; bits 0..6 are the low part of the mask,
|
||||
// hi byte holds bits 7..14 (back at bit 7, forward at bit 8).
|
||||
mask = (mask & 0x7f) | uint16(hi[0])<<7
|
||||
}
|
||||
|
||||
s.pointerMu.Lock()
|
||||
s.lastPointerX = x
|
||||
s.lastPointerY = y
|
||||
s.pointerMu.Unlock()
|
||||
s.injector.InjectPointer(buttonMask, x, y, s.serverW, s.serverH)
|
||||
s.injector.InjectPointer(mask, x, y, s.serverW, s.serverH)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -269,6 +269,28 @@ func (s *session) sendDesktopSize(w, h int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// sendExtMouseAck emits the pseudo-rect that flips the client into
|
||||
// ExtendedMouseButtons mode, where mouse-back and mouse-forward are
|
||||
// carried in a second mask byte. The rect has zero geometry and no
|
||||
// body; the encoding number alone is the signal.
|
||||
func (s *session) sendExtMouseAck() error {
|
||||
header := make([]byte, 4)
|
||||
header[0] = serverFramebufferUpdate
|
||||
binary.BigEndian.PutUint16(header[2:4], 1)
|
||||
|
||||
rect := make([]byte, 12)
|
||||
enc := int32(pseudoEncExtendedMouseButtons)
|
||||
binary.BigEndian.PutUint32(rect[8:12], uint32(enc))
|
||||
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
if _, err := s.conn.Write(header); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := s.conn.Write(rect)
|
||||
return err
|
||||
}
|
||||
|
||||
// refreshCopyRectIndex does a full hash sweep of the just-swapped prevFrame.
|
||||
// Used after full-frame sends, where we don't have a per-tile dirty list to
|
||||
// drive an incremental update.
|
||||
|
||||
@@ -35,7 +35,7 @@ func (s *StubInputInjector) InjectKeyScancode(_ uint32, _ uint32, _ bool) {
|
||||
}
|
||||
|
||||
// InjectPointer is a no-op on unsupported platforms.
|
||||
func (s *StubInputInjector) InjectPointer(_ uint8, _, _, _, _ int) {
|
||||
func (s *StubInputInjector) InjectPointer(_ uint16, _, _, _, _ int) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user