diff --git a/server/web/index.html b/server/web/index.html index 420ae6b..e88a887 100644 --- a/server/web/index.html +++ b/server/web/index.html @@ -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,') 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,'); } .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,'); + } + /* 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,'); + } + /* IDC_WAIT (15) — busy hourglass */ + .cursor-overlay.cursor-wait { + --hx: 12px; --hy: 12px; + background-image: url('data:image/svg+xml,'); + } + /* IDC_NO (7) — forbidden */ + .cursor-overlay.cursor-no { + --hx: 12px; --hy: 12px; + background-image: url('data:image/svg+xml,'); + } + /* IDC_SIZEALL (9) — 4-way move */ + .cursor-overlay.cursor-move { + --hx: 12px; --hy: 12px; + background-image: url('data:image/svg+xml,'); + } + /* IDC_SIZENS (11) — vertical resize */ + .cursor-overlay.cursor-sizens { + --hx: 12px; --hy: 12px; + background-image: url('data:image/svg+xml,'); + } + /* IDC_SIZEWE (13) — horizontal resize */ + .cursor-overlay.cursor-sizewe { + --hx: 12px; --hy: 12px; + background-image: url('data:image/svg+xml,'); + } + /* 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,'); + } + /* 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,'); + } /* 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