Complete browser-based remote desktop client built from protocol analysis: - Protobuf message definitions (75+ message types, 10 enums) - NaCl encryption (Ed25519 + Curve25519 ECDH + XSalsa20-Poly1305) - WebSocket signaling (hbbs) + relay (hbbr) connection lifecycle - VP8/VP9/AV1 video decoding via ogvjs WASM + yuv-canvas WebGL - Opus audio decoding via libopus WASM + Web Audio API - Full mouse/keyboard input forwarding to protobuf events - Connection dialog, status bar, toolbar, log panel UI Also tracked web_deps (codec libraries) in rustdesk-as-ref/
413 lines
12 KiB
JavaScript
413 lines
12 KiB
JavaScript
/**
|
|
* RustDesk Standalone Web Client - Input Handler
|
|
*
|
|
* Captures mouse and keyboard events from the remote desktop canvas
|
|
* and converts them to RustDesk protocol messages.
|
|
*/
|
|
|
|
const RDInput = (() => {
|
|
let _canvas = null;
|
|
let _enabled = false;
|
|
let _displayWidth = 0;
|
|
let _displayHeight = 0;
|
|
let _scaleX = 1;
|
|
let _scaleY = 1;
|
|
let _mouseButtons = 0; // Current mouse button mask
|
|
let _modifiers = []; // Current modifier keys
|
|
let _keyboardLocked = false;
|
|
let _viewOnly = false;
|
|
|
|
// Mouse button masks
|
|
const MOUSE = {
|
|
LEFT: 1,
|
|
RIGHT: 2,
|
|
MIDDLE: 4,
|
|
BUTTON4: 8,
|
|
BUTTON5: 16
|
|
};
|
|
|
|
// Modifier key mappings
|
|
const MODIFIER_MAP = {
|
|
'ControlLeft': 50, // LeftControl
|
|
'ControlRight': 54, // RightControl
|
|
'ShiftLeft': 51, // LeftShift
|
|
'ShiftRight': 55, // RightShift
|
|
'AltLeft': 52, // LeftAlt
|
|
'AltRight': 56, // RightAlt
|
|
'MetaLeft': 53, // LeftMeta
|
|
'MetaRight': 57, // RightMeta
|
|
};
|
|
|
|
// Key name to ControlKey enum mapping
|
|
const KEY_MAP = {
|
|
'Alt': 1, 'Backspace': 2, 'CapsLock': 3, 'Control': 4, 'Delete': 5,
|
|
'ArrowDown': 6, 'End': 7, 'Escape': 8,
|
|
'F1': 9, 'F10': 10, 'F11': 11, 'F12': 12,
|
|
'F2': 13, 'F3': 14, 'F4': 15, 'F5': 16, 'F6': 17,
|
|
'F7': 18, 'F8': 19, 'F9': 20,
|
|
'Home': 21, 'ArrowLeft': 22, 'Meta': 23,
|
|
'PageDown': 24, 'PageUp': 25, 'Enter': 26, 'Return': 26,
|
|
'ArrowRight': 27, 'Shift': 28, ' ': 29, 'Space': 29,
|
|
'Tab': 30, 'ArrowUp': 31,
|
|
'Insert': 49,
|
|
'NumLock': 63, 'ScrollLock': 64,
|
|
'PrintScreen': 66,
|
|
'Numpad0': 32, 'Numpad1': 33, 'Numpad2': 34, 'Numpad3': 35,
|
|
'Numpad4': 36, 'Numpad5': 37, 'Numpad6': 38, 'Numpad7': 39,
|
|
'Numpad8': 40, 'Numpad9': 41,
|
|
'NumpadAdd': 42, 'NumpadDecimal': 43, 'NumpadDelete': 44,
|
|
'NumpadDivide': 45, 'NumpadEnter': 46,
|
|
'NumpadMultiply': 47, 'NumpadSubtract': 48,
|
|
'VolumeDown': 58, 'VolumeMute': 59, 'VolumeUp': 60,
|
|
'ContextMenu': 61,
|
|
};
|
|
|
|
/**
|
|
* Initialize input handler
|
|
* @param {HTMLCanvasElement} canvas - The canvas to capture events from
|
|
*/
|
|
function init(canvas) {
|
|
_canvas = canvas;
|
|
attachMouseEvents();
|
|
attachKeyboardEvents();
|
|
attachTouchEvents();
|
|
attachWheelEvents();
|
|
console.log('[RDInput] Initialized');
|
|
}
|
|
|
|
/**
|
|
* Enable/disable input capture
|
|
*/
|
|
function setEnabled(enabled) {
|
|
_enabled = enabled;
|
|
if (enabled) {
|
|
_canvas.style.cursor = 'default';
|
|
} else {
|
|
_canvas.style.cursor = 'not-allowed';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set view-only mode
|
|
*/
|
|
function setViewOnly(viewOnly) {
|
|
_viewOnly = viewOnly;
|
|
}
|
|
|
|
/**
|
|
* Update display dimensions for coordinate mapping
|
|
*/
|
|
function setDisplaySize(width, height) {
|
|
_displayWidth = width;
|
|
_displayHeight = height;
|
|
updateScale();
|
|
}
|
|
|
|
/**
|
|
* Calculate scale factors for coordinate mapping
|
|
*/
|
|
function updateScale() {
|
|
if (_canvas && _displayWidth > 0 && _displayHeight > 0) {
|
|
_scaleX = _displayWidth / _canvas.clientWidth;
|
|
_scaleY = _displayHeight / _canvas.clientHeight;
|
|
} else {
|
|
_scaleX = 1;
|
|
_scaleY = 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert canvas coordinates to display coordinates
|
|
*/
|
|
function canvasToDisplay(canvasX, canvasY) {
|
|
return {
|
|
x: Math.round(canvasX * _scaleX),
|
|
y: Math.round(canvasY * _scaleY)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Attach mouse event listeners
|
|
*/
|
|
function attachMouseEvents() {
|
|
_canvas.addEventListener('mousedown', onMouseDown);
|
|
_canvas.addEventListener('mouseup', onMouseUp);
|
|
_canvas.addEventListener('mousemove', onMouseMove);
|
|
_canvas.addEventListener('mouseenter', onMouseEnter);
|
|
_canvas.addEventListener('mouseleave', onMouseLeave);
|
|
_canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
}
|
|
|
|
/**
|
|
* Attach keyboard event listeners
|
|
*/
|
|
function attachKeyboardEvents() {
|
|
document.addEventListener('keydown', onKeyDown);
|
|
document.addEventListener('keyup', onKeyUp);
|
|
}
|
|
|
|
/**
|
|
* Attach touch event listeners
|
|
*/
|
|
function attachTouchEvents() {
|
|
_canvas.addEventListener('touchstart', onTouchStart, { passive: false });
|
|
_canvas.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
_canvas.addEventListener('touchend', onTouchEnd, { passive: false });
|
|
_canvas.addEventListener('touchcancel', onTouchEnd, { passive: false });
|
|
}
|
|
|
|
/**
|
|
* Attach wheel event listeners
|
|
*/
|
|
function attachWheelEvents() {
|
|
_canvas.addEventListener('wheel', onWheel, { passive: false });
|
|
}
|
|
|
|
/**
|
|
* Handle mouse down
|
|
*/
|
|
function onMouseDown(e) {
|
|
if (!_enabled || _viewOnly) return;
|
|
e.preventDefault();
|
|
|
|
updateModifiers(e);
|
|
|
|
let buttonMask = 0;
|
|
switch (e.button) {
|
|
case 0: buttonMask = MOUSE.LEFT; break;
|
|
case 1: buttonMask = MOUSE.MIDDLE; break;
|
|
case 2: buttonMask = MOUSE.RIGHT; break;
|
|
case 3: buttonMask = MOUSE.BUTTON4; break;
|
|
case 4: buttonMask = MOUSE.BUTTON5; break;
|
|
}
|
|
|
|
_mouseButtons |= buttonMask;
|
|
|
|
const pos = canvasToDisplay(e.offsetX, e.offsetY);
|
|
RDConnection.sendMouseEvent(_mouseButtons | getModifierMask(), pos.x, pos.y, getModifierList());
|
|
|
|
// Focus for keyboard events
|
|
_canvas.focus();
|
|
}
|
|
|
|
/**
|
|
* Handle mouse up
|
|
*/
|
|
function onMouseUp(e) {
|
|
if (!_enabled || _viewOnly) return;
|
|
e.preventDefault();
|
|
|
|
updateModifiers(e);
|
|
|
|
let buttonMask = 0;
|
|
switch (e.button) {
|
|
case 0: buttonMask = MOUSE.LEFT; break;
|
|
case 1: buttonMask = MOUSE.MIDDLE; break;
|
|
case 2: buttonMask = MOUSE.RIGHT; break;
|
|
case 3: buttonMask = MOUSE.BUTTON4; break;
|
|
case 4: buttonMask = MOUSE.BUTTON5; break;
|
|
}
|
|
|
|
_mouseButtons &= ~buttonMask;
|
|
|
|
const pos = canvasToDisplay(e.offsetX, e.offsetY);
|
|
RDConnection.sendMouseEvent(_mouseButtons | getModifierMask(), pos.x, pos.y, getModifierList());
|
|
}
|
|
|
|
/**
|
|
* Handle mouse move
|
|
*/
|
|
function onMouseMove(e) {
|
|
if (!_enabled || _viewOnly) return;
|
|
|
|
const pos = canvasToDisplay(e.offsetX, e.offsetY);
|
|
RDConnection.sendMouseEvent(_mouseButtons | getModifierMask(), pos.x, pos.y, getModifierList());
|
|
}
|
|
|
|
/**
|
|
* Handle mouse enter
|
|
*/
|
|
function onMouseEnter(e) {
|
|
// Notify peer that mouse entered the canvas
|
|
}
|
|
|
|
/**
|
|
* Handle mouse leave
|
|
*/
|
|
function onMouseLeave(e) {
|
|
if (!_enabled || _viewOnly) return;
|
|
|
|
// Send a release event when mouse leaves
|
|
RDConnection.sendMouseEvent(0, -1, -1, []);
|
|
_mouseButtons = 0;
|
|
}
|
|
|
|
/**
|
|
* Handle mouse wheel
|
|
*/
|
|
function onWheel(e) {
|
|
if (!_enabled || _viewOnly) return;
|
|
e.preventDefault();
|
|
|
|
const buttonMask = e.deltaY > 0 ? MOUSE.BUTTON5 : MOUSE.BUTTON4;
|
|
const pos = canvasToDisplay(e.offsetX, e.offsetY);
|
|
|
|
// Press
|
|
RDConnection.sendMouseEvent(buttonMask | getModifierMask(), pos.x, pos.y, getModifierList());
|
|
// Release
|
|
setTimeout(() => {
|
|
RDConnection.sendMouseEvent(getModifierMask(), pos.x, pos.y, getModifierList());
|
|
}, 20);
|
|
}
|
|
|
|
/**
|
|
* Handle touch start
|
|
*/
|
|
function onTouchStart(e) {
|
|
if (!_enabled || _viewOnly) return;
|
|
e.preventDefault();
|
|
|
|
if (e.touches.length === 1) {
|
|
const touch = e.touches[0];
|
|
const rect = _canvas.getBoundingClientRect();
|
|
const x = touch.clientX - rect.left;
|
|
const y = touch.clientY - rect.top;
|
|
const pos = canvasToDisplay(x, y);
|
|
|
|
RDConnection.sendMouseEvent(MOUSE.LEFT | getModifierMask(), pos.x, pos.y, getModifierList());
|
|
_mouseButtons = MOUSE.LEFT;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle touch move
|
|
*/
|
|
function onTouchMove(e) {
|
|
if (!_enabled || _viewOnly) return;
|
|
e.preventDefault();
|
|
|
|
if (e.touches.length === 1) {
|
|
const touch = e.touches[0];
|
|
const rect = _canvas.getBoundingClientRect();
|
|
const x = touch.clientX - rect.left;
|
|
const y = touch.clientY - rect.top;
|
|
const pos = canvasToDisplay(x, y);
|
|
|
|
RDConnection.sendMouseEvent(MOUSE.LEFT | getModifierMask(), pos.x, pos.y, getModifierList());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle touch end
|
|
*/
|
|
function onTouchEnd(e) {
|
|
if (!_enabled || _viewOnly) return;
|
|
e.preventDefault();
|
|
|
|
RDConnection.sendMouseEvent(getModifierMask(), -1, -1, []);
|
|
_mouseButtons = 0;
|
|
}
|
|
|
|
/**
|
|
* Handle key down
|
|
*/
|
|
function onKeyDown(e) {
|
|
if (!_enabled || _viewOnly) return;
|
|
|
|
// Don't capture if we're typing in an input field
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
updateModifiers(e);
|
|
|
|
const controlKey = KEY_MAP[e.key] || KEY_MAP[e.code];
|
|
if (controlKey) {
|
|
// Special key
|
|
RDConnection.sendKeyEvent(controlKey, true, false, getModifierList());
|
|
} else {
|
|
// Regular character
|
|
const charCode = e.key.charCodeAt(0);
|
|
if (charCode > 0 && charCode < 65536) {
|
|
RDConnection.sendCharEvent(charCode, true, false, getModifierList());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle key up
|
|
*/
|
|
function onKeyUp(e) {
|
|
if (!_enabled || _viewOnly) return;
|
|
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
updateModifiers(e);
|
|
|
|
const controlKey = KEY_MAP[e.key] || KEY_MAP[e.code];
|
|
if (controlKey) {
|
|
RDConnection.sendKeyEvent(controlKey, false, false, getModifierList());
|
|
} else {
|
|
const charCode = e.key.charCodeAt(0);
|
|
if (charCode > 0 && charCode < 65536) {
|
|
RDConnection.sendCharEvent(charCode, false, false, getModifierList());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update current modifier state
|
|
*/
|
|
function updateModifiers(e) {
|
|
_modifiers = [];
|
|
if (e.ctrlKey || e.metaKey) _modifiers.push(4); // Control
|
|
if (e.shiftKey) _modifiers.push(28); // Shift
|
|
if (e.altKey) _modifiers.push(1); // Alt
|
|
}
|
|
|
|
/**
|
|
* Get current modifier key list
|
|
*/
|
|
function getModifierList() {
|
|
return [..._modifiers];
|
|
}
|
|
|
|
/**
|
|
* Get modifier bitmask for mouse events
|
|
*/
|
|
function getModifierMask() {
|
|
let mask = 0;
|
|
if (_modifiers.includes(4)) mask |= 0x10; // Ctrl
|
|
if (_modifiers.includes(28)) mask |= 0x20; // Shift
|
|
if (_modifiers.includes(1)) mask |= 0x40; // Alt
|
|
if (_modifiers.includes(23)) mask |= 0x80; // Meta
|
|
return mask;
|
|
}
|
|
|
|
/**
|
|
* Type a string of text (for paste/keyboard input)
|
|
*/
|
|
function typeString(text) {
|
|
if (!_enabled || _viewOnly) return;
|
|
|
|
for (const char of text) {
|
|
const charCode = char.charCodeAt(0);
|
|
RDConnection.sendCharEvent(charCode, true, true, getModifierList());
|
|
}
|
|
}
|
|
|
|
return {
|
|
init,
|
|
setEnabled,
|
|
setViewOnly,
|
|
setDisplaySize,
|
|
typeString,
|
|
MOUSE,
|
|
get enabled() { return _enabled; }
|
|
};
|
|
})();
|