projects/shelled/rustdesk-web-client/js/input.js
Z User 7b758ebe01 Add standalone RustDesk web client (reverse-engineered)
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/
2026-04-06 21:11:21 +00:00

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; }
};
})();