Improve(Web): Touch-mode visual cursor follows remote IDC_* state

This commit is contained in:
yuanyuanxiang
2026-05-17 20:02:10 +02:00
parent 4e0627e6a3
commit ccab37658a

View File

@@ -951,6 +951,9 @@
flex-direction: column;
}
}
/* Cursor overlay (touchpad mode). Per-variant classes are swapped via JS based on
the remote IDC_* index. Each variant overrides --hx/--hy so the icon's tip
(hotspot), not the div's top-left, lands on the reported cursor position. */
.cursor-overlay {
position: fixed;
width: 24px;
@@ -958,15 +961,65 @@
pointer-events: none;
display: none;
z-index: 1002;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="%23fff" stroke="%23000" stroke-width="1.5" d="M4 4l7 17 2.5-6.5L20 12z"/></svg>') no-repeat;
background-repeat: no-repeat;
background-size: contain;
filter: drop-shadow(2px 2px 3px rgba(0,0,0,0.6));
transform-origin: 0 0;
/* SVG arrow tip is at (4,4) inside the 24x24 viewBox; shift overlay so the tip,
not the div's top-left, lands on the reported cursor position. */
transform: translate(-4px, -4px);
--hx: 4px;
--hy: 4px;
transform: translate(calc(-1 * var(--hx)), calc(-1 * var(--hy)));
/* IDC_ARROW (default) — also the fallback for unmapped indices. */
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" stroke="black" stroke-width="1.5" d="M4 4l7 17 2.5-6.5L20 12z"/></svg>');
}
.cursor-overlay.active { display: block; }
/* IDC_IBEAM (5) — text input */
.cursor-overlay.cursor-ibeam {
--hx: 8px; --hy: 12px;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="white" stroke-width="3" d="M4 3h8M8 3v18M4 21h8"/><path fill="none" stroke="black" stroke-width="1" d="M4 3h8M8 3v18M4 21h8"/></svg>');
}
/* IDC_HAND (3) — Windows-style pointing hand: tall index finger, thumb on the left,
three visible bent fingers (middle/ring/pinky) with descending heights, rounded palm. */
.cursor-overlay.cursor-hand {
--hx: 11.5px; --hy: 1.5px;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" stroke="black" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round" d="M10 3 a1.5 1.5 0 0 1 3 0 v7 q1.5 -7 3 0 q1.5 -6 3 0 q1.5 -5 3 0 q1 1 1 2 v5 a4 4 0 0 1 -4 4 h-7 a4 4 0 0 1 -4 -4 v-2 q-2 0 -2 -2 q0 -2 2 -2 q1 0 1 1 q1 -1 1 -2 z"/></svg>');
}
/* IDC_WAIT (15) — busy hourglass */
.cursor-overlay.cursor-wait {
--hx: 12px; --hy: 12px;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round" d="M6 3h12v4l-5 5 5 5v4H6v-4l5-5-5-5z"/></svg>');
}
/* IDC_NO (7) — forbidden */
.cursor-overlay.cursor-no {
--hx: 12px; --hy: 12px;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" fill="none" stroke="white" stroke-width="3"/><line x1="6.5" y1="6.5" x2="17.5" y2="17.5" stroke="white" stroke-width="3"/><circle cx="12" cy="12" r="8" fill="none" stroke="black" stroke-width="1.5"/><line x1="6.5" y1="6.5" x2="17.5" y2="17.5" stroke="black" stroke-width="1.5"/></svg>');
}
/* IDC_SIZEALL (9) — 4-way move */
.cursor-overlay.cursor-move {
--hx: 12px; --hy: 12px;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round" d="M12 3 9 7h2v4H7V9l-4 3 4 3v-2h4v4H9l3 4 3-4h-2v-4h4v2l4-3-4-3v2h-4V7h2z"/></svg>');
}
/* IDC_SIZENS (11) — vertical resize */
.cursor-overlay.cursor-sizens {
--hx: 12px; --hy: 12px;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round" d="M12 3 9 7h2v10H9l3 4 3-4h-2V7h2z"/></svg>');
}
/* IDC_SIZEWE (13) — horizontal resize */
.cursor-overlay.cursor-sizewe {
--hx: 12px; --hy: 12px;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round" d="M3 12 7 9v2h10V9l4 3-4 3v-2H7v2z"/></svg>');
}
/* IDC_SIZENWSE (12) — diagonal NW-SE resize (\\). Rotating vertical NS arrow -45° (CCW)
tilts the top-left to upper-left and bottom-right to lower-right. */
.cursor-overlay.cursor-sizenwse {
--hx: 12px; --hy: 12px;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g transform="rotate(-45 12 12)"><path fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round" d="M12 3 9 7h2v10H9l3 4 3-4h-2V7h2z"/></g></svg>');
}
/* IDC_SIZENESW (10) — diagonal NE-SW resize (//). Rotating vertical NS arrow +45° (CW)
tilts the top toward upper-right and bottom toward lower-left. */
.cursor-overlay.cursor-sizenesw {
--hx: 12px; --hy: 12px;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g transform="rotate(45 12 12)"><path fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round" d="M12 3 9 7h2v10H9l3 4 3-4h-2V7h2z"/></g></svg>');
}
/* Input shortcut bar - below canvas, portrait mode only */
.input-shortcuts {
position: absolute;
@@ -1465,15 +1518,11 @@
initDecoder(msg.width, msg.height);
break;
case 'cursor':
// Update remote cursor style (only for desktop in control mode)
// Update remote cursor visual: native CSS cursor on desktop,
// overlay variant on touch. 254=custom (unsupported), 255=unsupported.
currentCursorIndex = msg.index;
if (controlEnabled && !isTouchDevice) {
const canvas = document.getElementById('screen-canvas');
// 254=custom cursor (not supported in web), 255=unsupported -> default
const cssCursor = (msg.index >= 0 && msg.index < cursorMap.length)
? cursorMap[msg.index]
: 'default';
canvas.style.cursor = cssCursor;
if (controlEnabled) {
applyRemoteCursor(msg.index);
}
break;
case 'device_offline':
@@ -2274,6 +2323,39 @@
];
let currentCursorIndex = 1; // Default: arrow
// IDC_* index -> CSS class on .cursor-overlay (touch-mode visual cursor).
// Indices not listed (0,2,4,6,8,14, custom) fall back to default arrow.
const cursorOverlayClassMap = {
3: 'cursor-hand',
5: 'cursor-ibeam',
7: 'cursor-no',
9: 'cursor-move',
10: 'cursor-sizenesw',
11: 'cursor-sizens',
12: 'cursor-sizenwse',
13: 'cursor-sizewe',
15: 'cursor-wait'
};
// Apply the remote cursor: native CSS cursor on desktop, overlay variant on touch.
function applyRemoteCursor(index) {
if (isTouchDevice) {
const overlay = document.getElementById('cursor-overlay');
if (!overlay) return;
const wasActive = overlay.classList.contains('active');
overlay.className = 'cursor-overlay' + (wasActive ? ' active' : '');
const cls = cursorOverlayClassMap[index];
if (cls) overlay.classList.add(cls);
} else {
const canvas = document.getElementById('screen-canvas');
if (!canvas) return;
const cssCursor = (index >= 0 && index < cursorMap.length)
? cursorMap[index]
: 'default';
canvas.style.cursor = cssCursor;
}
}
// Floating toolbar state
let toolbarVisible = false;
let toolbarHideTimer = null;
@@ -2424,20 +2506,17 @@
// Cursor handling
const canvas = document.getElementById('screen-canvas');
const cursorOverlay = document.getElementById('cursor-overlay');
// Touch devices: hide browser cursor, show overlay (touchpad mode)
// Desktop: use remote cursor style when control enabled
// Touch: hide native cursor and show our overlay (touchpad mode).
// Desktop: native CSS cursor is set by applyRemoteCursor below.
if (controlEnabled && isTouchDevice) {
canvas.style.cursor = 'none';
} else if (controlEnabled && !isTouchDevice) {
// Apply current remote cursor
const cssCursor = (currentCursorIndex >= 0 && currentCursorIndex < cursorMap.length)
? cursorMap[currentCursorIndex]
: 'default';
canvas.style.cursor = cssCursor;
} else {
} else if (!controlEnabled) {
canvas.style.cursor = 'default';
}
cursorOverlay.classList.toggle('active', controlEnabled && isTouchDevice);
if (controlEnabled) {
applyRemoteCursor(currentCursorIndex);
}
}
// Update cursor overlay position (accounting for zoom/pan transform)
@@ -3283,7 +3362,8 @@
const qcMouse = document.getElementById('qc-mouse');
if (qcMouse) qcMouse.classList.remove('active');
document.getElementById('screen-canvas').style.cursor = 'default';
document.getElementById('cursor-overlay').classList.remove('active');
// Strip any cursor-* variant on the overlay, leaving the bare default arrow.
document.getElementById('cursor-overlay').className = 'cursor-overlay';
currentCursorIndex = 1; // Reset to default arrow
// Reset zoom state