From 80f95a41b2aea38a1a026d75afbe3e3eb4e435d4 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Mon, 20 Apr 2026 23:56:39 +0200 Subject: [PATCH] Improve: Web remote desktop two-finger gesture recognition --- build.ps1 | 2 +- server/2015Remote/WebPage.h | 150 ++++++++++++++++++++++++------ test/unit/auth/DateVerifyTest.cpp | 18 ++-- 3 files changed, 134 insertions(+), 36 deletions(-) diff --git a/build.ps1 b/build.ps1 index 360193a..f8bd28e 100644 --- a/build.ps1 +++ b/build.ps1 @@ -61,7 +61,7 @@ elseif ($msBuild -match "\\18\\") { $vsYear = "2019 Insiders" } Write-Host "Using MSBuild: $msBuild" -ForegroundColor Cyan $rootDir = $PSScriptRoot -$slnFile = Join-Path $rootDir "2019Remote.sln" +$slnFile = Join-Path $rootDir "YAMA.sln" $upxPath = Join-Path $rootDir "server\2015Remote\res\3rd\upx.exe" # Publish mode overrides diff --git a/server/2015Remote/WebPage.h b/server/2015Remote/WebPage.h index 1e230bd..e2459f5 100644 --- a/server/2015Remote/WebPage.h +++ b/server/2015Remote/WebPage.h @@ -1325,8 +1325,9 @@ inline std::string GetWebPageHTML() { const canvasTop = containerRect.top + (containerRect.height - canvasDisplayHeight) / 2; // Convert canvas coords to position on unzoomed canvas - const relX = canvasX / canvas.width; // 0-1 - const relY = canvasY / canvas.height; // 0-1 + // Map [0, width-1] to [0, 1] for proper edge-to-edge display + const relX = canvas.width > 1 ? canvasX / (canvas.width - 1) : 0; + const relY = canvas.height > 1 ? canvasY / (canvas.height - 1) : 0; // Position on unzoomed canvas (in pixels from canvas top-left) const unzoomedX = relX * canvasDisplayWidth; @@ -1349,6 +1350,52 @@ inline std::string GetWebPageHTML() { cursorOverlay.style.top = screenY + 'px'; } + // Inverse transform: screen position -> canvas coordinates + // Used to recalculate cursor position after zoom/pan + function screenToCanvas(screenX, screenY) { + const canvas = document.getElementById('screen-canvas'); + const container = document.querySelector('.canvas-container'); + const containerRect = container.getBoundingClientRect(); + + const canvasDisplayWidth = canvas.offsetWidth; + const canvasDisplayHeight = canvas.offsetHeight; + const canvasLeft = containerRect.left + (containerRect.width - canvasDisplayWidth) / 2; + const canvasTop = containerRect.top + (containerRect.height - canvasDisplayHeight) / 2; + + // Reverse the transform chain + const scaledX = screenX - canvasLeft; + const scaledY = screenY - canvasTop; + + const originX = canvasDisplayWidth * zoomState.originX / 100; + const originY = canvasDisplayHeight * zoomState.originY / 100; + + // Reverse: scaledX = originX + (unzoomedX - originX) * scale + translateX * scale + const unzoomedX = (scaledX - originX) / zoomState.scale + originX - zoomState.translateX; + const unzoomedY = (scaledY - originY) / zoomState.scale + originY - zoomState.translateY; + + // Reverse: unzoomedX = relX * canvasDisplayWidth + const relX = canvasDisplayWidth > 0 ? unzoomedX / canvasDisplayWidth : 0; + const relY = canvasDisplayHeight > 0 ? unzoomedY / canvasDisplayHeight : 0; + + // Reverse: relX = canvasX / (canvas.width - 1) + const canvasX = relX * (canvas.width - 1); + const canvasY = relY * (canvas.height - 1); + + // Clamp to valid range + return { + x: Math.max(0, Math.min(canvas.width - 1, canvasX)), + y: Math.max(0, Math.min(canvas.height - 1, canvasY)) + }; + } +)HTML"; + + // Part 14b: JavaScript - Zoom state and touch helpers + html += R"HTML( + // Two-finger gesture constants + const ZOOM_THRESHOLD = 0.05; // 5% distance change to trigger zoom + const SCROLL_SENSITIVITY = 3; // Scroll speed multiplier + const SCROLL_DEADZONE = 2; // Minimum scroll delta to send + // Pinch-to-zoom state let zoomState = { scale: 1, @@ -1362,7 +1409,11 @@ inline std::string GetWebPageHTML() { pinchCenterY: 0, // Transform origin relative to canvas (percentage) originX: 50, - originY: 50 + originY: 50, + // Two-finger gesture detection + hasZoomed: false, // Whether zoom occurred in current gesture + lastScrollY: 0, // For scroll delta calculation + initialPinchDist: 0 // Distance at gesture start (for cumulative detection) }; const zoomIndicator = document.getElementById('zoom-indicator'); let zoomIndicatorTimer = null; @@ -1469,8 +1520,8 @@ inline std::string GetWebPageHTML() { initCursor(); // Sensitivity multiplier (adjust for comfortable control) const sensitivity = 1.5; - cursorState.x = Math.max(0, Math.min(canvas.width, cursorState.x + dx * sensitivity)); - cursorState.y = Math.max(0, Math.min(canvas.height, cursorState.y + dy * sensitivity)); + cursorState.x = Math.max(0, Math.min(canvas.width - 1, cursorState.x + dx * sensitivity)); + cursorState.y = Math.max(0, Math.min(canvas.height - 1, cursorState.y + dy * sensitivity)); updateCursorOverlay(cursorState.x, cursorState.y); // Send move to remote sendMouse('move', Math.round(cursorState.x), Math.round(cursorState.y), 0); @@ -1538,12 +1589,16 @@ inline std::string GetWebPageHTML() { touchState.touchCount = e.touches.length; if (e.touches.length === 2) { - // Two finger touch - start pinch zoom + // Two finger touch - could be pinch zoom or scroll zoomState.isPinching = true; - zoomState.lastPinchDist = getPinchDistance(e.touches); + const initialDist = getPinchDistance(e.touches); + zoomState.initialPinchDist = initialDist; // For cumulative change detection + zoomState.lastPinchDist = initialDist; // For frame-by-frame zoom calculation + zoomState.hasZoomed = false; // Track if zoom occurred during this gesture const center = getPinchCenter(e.touches); zoomState.pinchCenterX = center.x; zoomState.pinchCenterY = center.y; + zoomState.lastScrollY = center.y; // For scroll delta calculation // Calculate pinch center relative to canvas for transform-origin // Only set origin when starting a new zoom (scale == 1) @@ -1627,35 +1682,63 @@ inline std::string GetWebPageHTML() { e.stopPropagation(); // Prevent exiting fullscreen if (e.touches.length === 2 && zoomState.isPinching) { - // Two finger move - pinch zoom AND pan simultaneously + // Two finger move - zoom+pan or scroll const newDist = getPinchDistance(e.touches); const newCenter = getPinchCenter(e.touches); + const frameDelta = newDist / zoomState.lastPinchDist; // Frame-by-frame change + const totalDelta = newDist / zoomState.initialPinchDist; // Cumulative change from gesture start - // Calculate zoom - const delta = newDist / zoomState.lastPinchDist; - const newScale = Math.max(zoomState.minScale, Math.min(zoomState.maxScale, zoomState.scale * delta)); + // Detect gesture type: zoom vs scroll + // Use CUMULATIVE change to detect zoom intent (catches slow pinch gestures) + // Also treat as zoom if already at scale boundary and trying to zoom further + const atMinScale = zoomState.scale <= zoomState.minScale; + const atMaxScale = zoomState.scale >= zoomState.maxScale; + const tryingToShrink = totalDelta < 1; // Use cumulative for direction + const tryingToEnlarge = totalDelta > 1; - // Calculate pan (movement of pinch center) - if (zoomState.scale > 1 || newScale > 1) { + if (Math.abs(totalDelta - 1) > ZOOM_THRESHOLD || + (atMinScale && tryingToShrink) || + (atMaxScale && tryingToEnlarge)) { + zoomState.hasZoomed = true; + } + + if (zoomState.hasZoomed) { + // Zoom + pan mode (once zoomed in this gesture, stay in this mode) + const newScale = Math.max(zoomState.minScale, Math.min(zoomState.maxScale, zoomState.scale * frameDelta)); const dx = newCenter.x - zoomState.pinchCenterX; const dy = newCenter.y - zoomState.pinchCenterY; - zoomState.translateX += dx / zoomState.scale; - zoomState.translateY += dy / zoomState.scale; - } - // Update state - zoomState.pinchCenterX = newCenter.x; - zoomState.pinchCenterY = newCenter.y; - zoomState.lastPinchDist = newDist; + // Pan when zoomed or zooming + if (zoomState.scale > 1 || newScale > 1) { + zoomState.translateX += dx / zoomState.scale; + zoomState.translateY += dy / zoomState.scale; + } - if (newScale !== zoomState.scale) { - zoomState.scale = newScale; - showZoomIndicator(); - } - applyZoomTransform(); - // Update cursor overlay to follow canvas transform - if (cursorState.initialized) { - updateCursorOverlay(cursorState.x, cursorState.y); + // Update state + zoomState.pinchCenterX = newCenter.x; + zoomState.pinchCenterY = newCenter.y; + zoomState.lastPinchDist = newDist; + + if (newScale !== zoomState.scale) { + zoomState.scale = newScale; + showZoomIndicator(); + } + applyZoomTransform(); + // Note: cursor overlay stays fixed on screen during zoom (Microsoft RD style) + // cursorState will be recalculated on touchend via screenToCanvas() + } else { + // Scroll mode (no zoom occurred yet) + const scrollDelta = newCenter.y - zoomState.lastScrollY; + if (Math.abs(scrollDelta) > SCROLL_DEADZONE) { + initCursor(); + // Send wheel event at cursor position + sendMouse('wheel', Math.round(cursorState.x), Math.round(cursorState.y), 0, -scrollDelta * SCROLL_SENSITIVITY); + zoomState.lastScrollY = newCenter.y; + } + // Always update state to prevent jump if user starts zooming + zoomState.pinchCenterX = newCenter.x; + zoomState.pinchCenterY = newCenter.y; + zoomState.lastPinchDist = newDist; } return; } @@ -1740,6 +1823,17 @@ inline std::string GetWebPageHTML() { // Handle pinch end if (zoomState.isPinching) { if (e.touches.length < 2) { + // Recalculate cursor position after zoom/pan (Microsoft RD style) + // Cursor stayed fixed on screen, so reverse-calculate its new canvas coords + if (cursorState.initialized && zoomState.hasZoomed) { + const cursorOverlay = document.getElementById('cursor-overlay'); + const cursorScreenX = parseFloat(cursorOverlay.style.left) || 0; + const cursorScreenY = parseFloat(cursorOverlay.style.top) || 0; + const newCoords = screenToCanvas(cursorScreenX, cursorScreenY); + cursorState.x = newCoords.x; + cursorState.y = newCoords.y; + } + zoomState.isPinching = false; // Update pan center and lastX/lastY for smooth transition if (e.touches.length === 1) { diff --git a/test/unit/auth/DateVerifyTest.cpp b/test/unit/auth/DateVerifyTest.cpp index 939b7d1..ee0a164 100644 --- a/test/unit/auth/DateVerifyTest.cpp +++ b/test/unit/auth/DateVerifyTest.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -88,16 +89,19 @@ private: int daysBetweenDates(const tm& date1, const tm& date2) { - auto timeToTimePoint = [](const tm& tmTime) { - std::time_t tt = mktime(const_cast(&tmTime)); - return std::chrono::system_clock::from_time_t(tt); + // Use Julian Day Number to avoid DST issues + // Formula: https://en.wikipedia.org/wiki/Julian_day + auto toJulianDay = [](int year, int month, int day) { + int a = (14 - month) / 12; + int y = year + 4800 - a; + int m = month + 12 * a - 3; + return day + (153 * m + 2) / 5 + 365 * y + y / 4 - y / 100 + y / 400 - 32045; }; - auto tp1 = timeToTimePoint(date1); - auto tp2 = timeToTimePoint(date2); + int jd1 = toJulianDay(date1.tm_year + 1900, date1.tm_mon + 1, date1.tm_mday); + int jd2 = toJulianDay(date2.tm_year + 1900, date2.tm_mon + 1, date2.tm_mday); - auto duration = tp1 > tp2 ? tp1 - tp2 : tp2 - tp1; - return static_cast(std::chrono::duration_cast(duration).count() / 24); + return std::abs(jd1 - jd2); } tm getCurrentDate()