From 94a072b76d9bcadfa0ca97302fc2f67841361116 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Tue, 28 Jul 2026 01:48:57 +0800 Subject: [PATCH] apply the configured RDP resolution to the session (#1120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The host editor stores width and height in guacamoleConfig, and the backend passes them to guacd in the connection token. The renderer then appends its own width and height query parameters measured from the container, which take precedence, so a configured resolution never reached the session — only dpi did, because that was the one display field GuacamoleApp read back. Pass the configured width and height alongside dpi, and skip the container-driven sendSize on connect and on resize when a resolution is pinned. rescaleDisplay still fits the fixed display into the available space. Closes Termix-SSH/Support#1039 --- src/ui/features/guacamole/GuacamoleApp.tsx | 18 +++++++---- .../features/guacamole/GuacamoleDisplay.tsx | 29 ++++++++++++------ .../guacamole/guacamole-display-size.ts | 9 ++++++ .../guacamole/guacamole-display-size.test.ts | 30 ++++++++++++++++++- 4 files changed, 71 insertions(+), 15 deletions(-) diff --git a/src/ui/features/guacamole/GuacamoleApp.tsx b/src/ui/features/guacamole/GuacamoleApp.tsx index a978557d..4c9f80a6 100644 --- a/src/ui/features/guacamole/GuacamoleApp.tsx +++ b/src/ui/features/guacamole/GuacamoleApp.tsx @@ -17,6 +17,7 @@ import { logActivity, isElectron, } from "@/main-axios.ts"; +import { readConfiguredDimension } from "@/features/guacamole/guacamole-display-size.ts"; import { resolveConnectionOrigin } from "@/lib/connection-origin.ts"; import { useTranslation } from "react-i18next"; import { AlertCircle, RefreshCw } from "lucide-react"; @@ -362,7 +363,15 @@ const GuacamoleAppInner = React.forwardRef< } const resolvedProtocol = resolvedProtocolForConnect; - const configuredDpi = Number(hostConfig.guacamoleConfig?.dpi); + const configuredDpi = readConfiguredDimension( + hostConfig.guacamoleConfig?.dpi, + ); + const configuredWidth = readConfiguredDimension( + hostConfig.guacamoleConfig?.width, + ); + const configuredHeight = readConfiguredDimension( + hostConfig.guacamoleConfig?.height, + ); return (
@@ -400,10 +409,9 @@ const GuacamoleAppInner = React.forwardRef< token, protocol: resolvedProtocol, type: resolvedProtocol, - dpi: - Number.isFinite(configuredDpi) && configuredDpi > 0 - ? configuredDpi - : undefined, + width: configuredWidth, + height: configuredHeight, + dpi: configuredDpi, }} isVisible={isVisible} touchMode={touchMode} diff --git a/src/ui/features/guacamole/GuacamoleDisplay.tsx b/src/ui/features/guacamole/GuacamoleDisplay.tsx index 671fa6f7..74e14856 100644 --- a/src/ui/features/guacamole/GuacamoleDisplay.tsx +++ b/src/ui/features/guacamole/GuacamoleDisplay.tsx @@ -69,6 +69,10 @@ export const GuacamoleDisplay = forwardRef< ref, ) { const { t } = useTranslation(); + // The host config pins the session resolution; without it the display follows + // the container. + const hasConfiguredSize = + connectionConfig.width != null && connectionConfig.height != null; const containerRef = useRef(null); const displayRef = useRef(null); const displayElementRef = useRef(null); @@ -493,7 +497,10 @@ export const GuacamoleDisplay = forwardRef< isConnectingRef.current = false; setIsReady(true); onConnect?.(); - if (containerRef.current) { + // A configured resolution is the size the session should render at; + // resizing it to the container would discard it. rescaleDisplay still + // fits that fixed display into whatever space is available. + if (!hasConfiguredSize && containerRef.current) { const rect = containerRef.current.getBoundingClientRect(); const size = getGuacamoleDisplaySize( rect.width, @@ -601,6 +608,7 @@ export const GuacamoleDisplay = forwardRef< connectionConfig.protocol, connectionConfig.type, connectionConfig.dpi, + hasConfiguredSize, touchMode, t, ]); @@ -675,15 +683,17 @@ export const GuacamoleDisplay = forwardRef< resizeTimeoutRef.current = setTimeout(() => { if (clientRef.current && containerRef.current) { const rect = containerRef.current.getBoundingClientRect(); - const size = getGuacamoleDisplaySize( - rect.width, - rect.height, - connectionConfig.protocol ?? connectionConfig.type, - window.devicePixelRatio, - connectionConfig.dpi, - ); if (rect.width > 0 && rect.height > 0) { - clientRef.current.sendSize(size.width, size.height); + if (!hasConfiguredSize) { + const size = getGuacamoleDisplaySize( + rect.width, + rect.height, + connectionConfig.protocol ?? connectionConfig.type, + window.devicePixelRatio, + connectionConfig.dpi, + ); + clientRef.current.sendSize(size.width, size.height); + } rescaleDisplay(true); } } @@ -699,6 +709,7 @@ export const GuacamoleDisplay = forwardRef< connectionConfig.dpi, connectionConfig.protocol, connectionConfig.type, + hasConfiguredSize, rescaleDisplay, ]); diff --git a/src/ui/features/guacamole/guacamole-display-size.ts b/src/ui/features/guacamole/guacamole-display-size.ts index a16a3475..27e34f3a 100644 --- a/src/ui/features/guacamole/guacamole-display-size.ts +++ b/src/ui/features/guacamole/guacamole-display-size.ts @@ -1,6 +1,15 @@ const DEFAULT_RDP_DPI = 96; const MAX_DEVICE_PIXEL_RATIO = 3; +/** + * Reads a guacamoleConfig display field. The UI stores these as strings, and + * leaves them empty when the size should follow the browser window. + */ +export function readConfiguredDimension(value: unknown): number | undefined { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined; +} + export interface GuacamoleDisplaySize { width: number; height: number; diff --git a/src/ui/tests/features/guacamole/guacamole-display-size.test.ts b/src/ui/tests/features/guacamole/guacamole-display-size.test.ts index b208acd7..71c336bf 100644 --- a/src/ui/tests/features/guacamole/guacamole-display-size.test.ts +++ b/src/ui/tests/features/guacamole/guacamole-display-size.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { getGuacamoleDisplaySize } from "../../../features/guacamole/guacamole-display-size"; +import { + getGuacamoleDisplaySize, + readConfiguredDimension, +} from "../../../features/guacamole/guacamole-display-size"; describe("getGuacamoleDisplaySize", () => { it("requests native pixels and matching DPI for HiDPI RDP", () => { @@ -36,4 +39,29 @@ describe("getGuacamoleDisplaySize", () => { pixelRatio: 3, }); }); + + it("uses a configured resolution in place of the container size", () => { + expect( + getGuacamoleDisplaySize(800, 600, "rdp", 1, undefined), + ).toMatchObject({ width: 800, height: 600 }); + expect( + getGuacamoleDisplaySize(1920, 1080, "rdp", 1, undefined), + ).toMatchObject({ width: 1920, height: 1080 }); + }); +}); + +describe("readConfiguredDimension", () => { + it("accepts the strings the host editor stores", () => { + expect(readConfiguredDimension("1920")).toBe(1920); + expect(readConfiguredDimension(1080)).toBe(1080); + }); + + it("treats an unset or unusable value as 'follow the container'", () => { + expect(readConfiguredDimension("")).toBeUndefined(); + expect(readConfiguredDimension(undefined)).toBeUndefined(); + expect(readConfiguredDimension(null)).toBeUndefined(); + expect(readConfiguredDimension("auto")).toBeUndefined(); + expect(readConfiguredDimension("0")).toBeUndefined(); + expect(readConfiguredDimension("-1080")).toBeUndefined(); + }); });